KavachOS is open source. Cloud launching soon.
kavachOS

01/TUTORIAL

How to add auth to a Next.js AI agentin ten minutes

A working app with user login, agent identity, and MCP OAuth, in ten minutes flat. Copy the code, ship it.

GD

Gagan Deep Singh

Founder, GLINCKER

Published

April 6, 20268 min read

You have a Next.js App Router app. It does something useful with AI. Now a user asks: “Can I log in? Can I give the agent access to my GitHub without handing it my password?” Good questions. Here is how to answer them in about ten minutes.

The goal is two things working at once. First, a human signs in with passkeys. Second, the agent gets its own identity with scoped permissions that trace back to that human. No shared API keys. No service accounts sitting on top of your prod database.


01

Project setup

Start with a fresh Next.js install or drop this into an existing project. The only hard requirement is App Router. Pages Router will need minor adjustments to the middleware pattern.

bash
npx create-next-app@latest my-agent-app --typescript --app
cd my-agent-app
npm install kavachos

Create a project at app.kavachos.com/sign-up and copy your API key. Then add it to .env.local:

bash.env.local
KAVACHOS_API_KEY=kv_live_xxxxxxxxxxxxxxxxxxxx

Next, create a single kavachOS instance you can import anywhere. I put this in lib/kavach.ts so it is never initialized twice:

typescriptlib/kavach.ts
import { createKavach } from 'kavachos';

if (!process.env.KAVACHOS_API_KEY) {
  throw new Error('KAVACHOS_API_KEY is not set');
}

export const kavach = createKavach({
  apiKey: process.env.KAVACHOS_API_KEY,
});

That is the whole setup. The SDK connects to kavachOS Cloud and keeps the connection warm. Everything else builds on this one export.


02

Human login with passkeys

Passkeys are the fastest path to production-quality auth. No password reset emails, no TOTP codes to copy. The browser handles the credential. Here is the route handler that starts a passkey registration:

typescriptapp/api/auth/passkey/register/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { kavach } from '@/lib/kavach';

export async function POST(req: NextRequest) {
  try {
    const { email } = await req.json() as { email: string };

    const options = await kavach.auth.passkey.beginRegistration({
      email,
      // displayName is shown in the OS passkey prompt
      displayName: email.split('@')[0],
    });

    return NextResponse.json(options);
  } catch (error) {
    console.error('[passkey register]', error);
    return NextResponse.json({ error: 'Failed to begin registration' }, { status: 500 });
  }
}

The finish step takes the credential from the browser and mints a session:

typescriptapp/api/auth/passkey/register/finish/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { kavach } from '@/lib/kavach';
import { cookies } from 'next/headers';

export async function POST(req: NextRequest) {
  const body = await req.json();

  const { session } = await kavach.auth.passkey.finishRegistration(body);

  // Set the session cookie (httpOnly, sameSite strict)
  const cookieStore = await cookies();
  cookieStore.set('kv_session', session.token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: session.expiresIn,
    path: '/',
  });

  return NextResponse.json({ userId: session.userId });
}

The sign-in flow mirrors this: one route to begin authentication, one to finish it. The Next.js adapter guide has the full browser-side code using the @simplewebauthn/browser client library.

Now wire up middleware so authenticated routes stay protected. A single matcher covers everything under /dashboard:

typescriptmiddleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { kavach } from '@/lib/kavach';

export async function middleware(req: NextRequest) {
  const token = req.cookies.get('kv_session')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  const { valid, userId } = await kavach.sessions.verify(token);

  if (!valid) {
    return NextResponse.redirect(new URL('/login', req.url));
  }

  const res = NextResponse.next();
  res.headers.set('x-user-id', userId);
  return res;
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

03

Creating an agent identity

The user is logged in. Now we need to give the agent its own credentials. This is the part that most auth libraries skip entirely. They let you pass the user session token directly to the agent. That works until the user revokes access or the agent needs narrower permissions than the user has.

kavachOS treats agents as first-class principals. Each agent gets an ID, a set of permissions, and a link back to the user who authorized it. Here is a route handler that creates an agent when a user starts a new session:

typescriptapp/api/agent/create/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { kavach } from '@/lib/kavach';

export async function POST(req: NextRequest) {
  const userId = req.headers.get('x-user-id');

  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const agent = await kavach.agents.create({
    name: 'coding-assistant',
    // Permissions are scoped strings you define. The format is yours.
    permissions: ['read:repos', 'read:issues', 'write:comments'],
    delegatedFrom: userId,
    // Agent token expires after 2 hours of inactivity
    ttl: '2h',
  });

  return NextResponse.json({
    agentId: agent.id,
    token: agent.token,
  });
}

Store agent.token in your session store or pass it to the AI SDK as a header. The token identifies the agent uniquely. When you verify it server-side, you get both the agent ID and the originating user ID back. The audit trail records both. See agent identity for the full permission model and expiry behavior.


04

Wiring to a route handler

Now the agent needs to call your app. It sends its token in the Authorization header. The route handler verifies it and checks permissions before doing any work:

typescriptapp/api/agent/search/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { kavach } from '@/lib/kavach';

export async function POST(req: NextRequest) {
  const authHeader = req.headers.get('authorization');
  const token = authHeader?.replace('Bearer ', '');

  if (!token) {
    return NextResponse.json({ error: 'Missing token' }, { status: 401 });
  }

  const identity = await kavach.agents.verify(token);

  if (!identity.valid) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }

  // Check the agent actually has this permission
  if (!identity.permissions.includes('read:repos')) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  const { query } = await req.json() as { query: string };

  // Your actual business logic here
  const results = await searchRepositories(query, identity.delegatedFrom);

  return NextResponse.json({ results });
}

async function searchRepositories(query: string, userId: string) {
  // Fetch using the user's connected OAuth token
  // ...
  return [];
}

The pattern is the same for every route the agent touches. Verify the token, check the specific permission, then proceed. Three lines of guard code before any business logic runs.

You can also pull React hooks into client components for session state. The Next.js adapter guide covers useSession and useAgentIdentity, both of which work server-side too via getServerSideSession.


05

Optional: delegation to sub-agents

Sometimes one agent spawns others. A research agent might create a summarizer and a citation checker. Each child should have narrower permissions than its parent and a shorter TTL. kavachOS enforces this automatically: you cannot delegate permissions the parent does not have.

typescriptapp/api/agent/delegate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { kavach } from '@/lib/kavach';

export async function POST(req: NextRequest) {
  const authHeader = req.headers.get('authorization');
  const parentToken = authHeader?.replace('Bearer ', '');

  if (!parentToken) {
    return NextResponse.json({ error: 'Missing token' }, { status: 401 });
  }

  const parent = await kavach.agents.verify(parentToken);

  if (!parent.valid) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }

  // Create a child agent with a subset of the parent's permissions
  const child = await kavach.agents.delegate({
    from: parent.agentId,
    // Must be a subset of parent.permissions
    permissions: ['read:repos'],
    ttl: '30m',
    purpose: 'code-search-subtask',
  });

  return NextResponse.json({ token: child.token });
}

The delegation chain is visible in the audit trail. If anything goes wrong mid-chain, you can trace exactly which agent made which call, in what order, and with what permissions active at the time. The delegation guide covers depth limits and how to revoke a chain mid-run.


Topics

  • #Next.js auth
  • #AI agent auth
  • #kavachOS tutorial
  • #agent identity
  • #App Router auth

Keep going in the docs

Read next

Share this post

Get started

Try kavachOS Cloud free

Free up to 1,000 MAU. Agent identities and MCP OAuth on every plan.