I hit this problem a few months back. About 50 agents, each talking to different MCP tool servers, and I couldn't answer a basic question: which agent did what, and who said it could?
Most auth libraries didn't help. They were built for humans logging into web apps. Sessions, cookies, redirect flows. My agents don't have browsers. They need their own tokens, scoped permissions, and when one spins up a sub-agent, I need some way to track that chain. Without it you're just staring at logs guessing which agent made which call.
I tried five libraries. Here's what I found.
TL;DR
KavachOS is the only one on this list built for agents. Quickstart here. Read on if you want the full picture.
At a glance
| Library | Agent identity | MCP OAuth | Delegation chains | Self-host | Edge runtime | License |
|---|---|---|---|---|---|---|
| Better Auth | — | — | — | ✓ | — | MIT |
| KavachOS | ✓ | ✓ | ✓ | ✓ | ✓ | MIT |
| Lucia | — | — | — | ✓ | — | MIT |
| Keycloak | — | — | — | ✓ | — | Apache 2.0 |
| Supabase Auth | — | — | — | ✓ | — | Apache 2.0 |
Agent capabilities
This is where things get thin. Only one library was actually built with agents in mind.
| Feature | Better Auth | KavachOS | Lucia | Keycloak | Supabase |
|---|---|---|---|---|---|
| Agent identity tokens | — | ✓ | — | — | — |
| Per-agent permission scoping | — | ✓ | — | Partial | — |
| Delegation chains | — | ✓ | — | — | — |
| Trust scoring | — | ✓ | — | — | — |
| Agent audit trail | — | ✓ | — | — | — |
| MCP OAuth 2.1 (PKCE, RFC 9728) | — | ✓ | — | — | — |
Human auth (all roughly equal here)
| Feature | Better Auth | KavachOS | Lucia | Keycloak | Supabase |
|---|---|---|---|---|---|
| Email/password | ✓ | ✓ | ✓ | ✓ | ✓ |
| Social OAuth | ✓ | ✓ | ✓ | ✓ | ✓ |
| Passkeys | ✓ | ✓ | — | ✓ | — |
| Magic link | ✓ | ✓ | — | — | ✓ |
| MFA | ✓ | ✓ | — | ✓ | ✓ |
| Enterprise SSO (SAML) | ✓ | ✓ | — | ✓ | Paid |
Infrastructure and DX
| Feature | Better Auth | KavachOS | Lucia | Keycloak | Supabase |
|---|---|---|---|---|---|
| TypeScript native | ✓ | ✓ | ✓ | — | — |
| Edge runtime (Workers, Deno) | — | ✓ | — | — | — |
| SQLite / D1 | ✓ | ✓ | ✓ | — | — |
| PostgreSQL | ✓ | ✓ | ✓ | ✓ | ✓ |
| npm install setup | ✓ | ✓ | ✓ | Docker | Hosted |
The core problem, visualized
Human auth (what most libraries give you):
Browser ──> Login page ──> Session cookie ──> API calls
└── expires, user re-authenticates
Agent auth (what you actually need):
Orchestrator Agent
│
├── Token (kv_abc..., scoped, 24h TTL)
│ └── Permissions: [repo:read, pr:comment]
│
├── Delegates to Sub-Agent
│ └── Token (kv_xyz..., subset, 1h TTL)
│ └── Permissions: [repo:read] only
│
└── Audit Trail
└── Every authorize() logged: agent, action, result, timestampSee the gap? Most libraries only handle the top half.
Better Auth
github.com/better-auth/better-auth
Best TypeScript auth library for regular apps, full stop. The plugin system covers basically anything you'd want for human auth. I've used it on two other projects and never had complaints.
import { betterAuth } from "better-auth";
const auth = betterAuth({
database: { provider: "sqlite", url: "./auth.db" },
emailAndPassword: { enabled: true },
plugins: [twoFactor(), organization()],
});I tried making it work for agents by creating "service users" with restricted roles. Got basic tokens going within a day. But then I needed delegation — agent A passing a subset of its permissions to agent B — and there's just no clean way to do that. The abstractions assume a human on the other end.
Also no MCP OAuth support. If you're running MCP tool servers, you'd need to build that layer yourself.
Perfectly good library. Wrong problem.
KavachOS
This is what I ended up using. I'll be upfront: I'm biased because it solved my problem. But I'll be honest about the rough edges too.
The difference is that agents are the primary concept here, not users. You create an AgentIdentity with scoped permissions, not a session with a cookie.
import { createKavach } from "kavachos";
const kavach = await createKavach({
database: { provider: "sqlite", url: "./kavach.db" },
});
const agent = await kavach.agent.create({
ownerId: "user-123",
name: "code-reviewer",
type: "autonomous",
permissions: [
{ resource: "mcp:github:*", actions: ["read"] },
{ resource: "mcp:github:pull_requests", actions: ["read", "comment"] },
],
});
// logged to audit trail automatically
const result = await kavach.authorize(agent.id, {
action: "read",
resource: "mcp:github:repos",
});
// delegate a subset to a sub-agent, 1h expiry, max 2 hops
await kavach.delegate({
fromAgent: agent.id,
toAgent: subAgent.id,
permissions: [{ resource: "mcp:github:issues", actions: ["read"] }],
expiresAt: new Date(Date.now() + 3600_000),
maxDepth: 2,
});The delegation part is what got me. My orchestrator creates sub-agents for specific tasks, each gets a time-limited slice of permissions that auto-expires. When a compliance person asked "show me every action this agent took last Tuesday" I exported the CSV in about 10 seconds. That felt good.
MCP OAuth 2.1 works out of the box. PKCE S256, resource indicators, server metadata discovery, dynamic client registration. I'm running the whole thing on Cloudflare Workers.
Now the bad parts. Community is small. If you hit a weird edge case you're reading source code, not Stack Overflow. The plugin ecosystem is thin compared to Better Auth. Docs are getting better but there are still gaps. If you only need human auth, don't use this — it's more machinery than the job needs.
Lucia
I'd used Lucia before and always liked the simplicity. Small API, clean types, no magic. The creator deprecated it in late 2024 in favor of oslo and arctic, but plenty of projects still run it.
import { Lucia } from "lucia";
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
const adapter = new DrizzleSQLiteAdapter(db, sessionTable, userTable);
const lucia = new Lucia(adapter, {
sessionCookie: { attributes: { secure: true } },
});
const session = await lucia.createSession(userId, {});For what it does, nothing to criticize. Clean session auth with no extra weight.
For agents it's a dead end. Sessions are a browser concept. There's no token model for background agents, no permissions, no audit trail. I thought about storing agent tokens in the session table for about five minutes before realizing I'd be fighting the library the whole way. And since it's deprecated, nothing new is coming.
Good for simple human auth. Wrong tool for agents.
Keycloak
The enterprise standard. Java, Red Hat-backed, been around since 2014. If you work at a company with more than 500 people, there's a decent chance Keycloak is running somewhere.
Keycloak keycloak = KeycloakBuilder.builder()
.serverUrl("http://localhost:8080")
.realm("master")
.clientId("admin-cli")
.username("admin")
.password("admin")
.build();
ClientRepresentation client = new ClientRepresentation();
client.setClientId("my-agent-service");
client.setServiceAccountsEnabled(true);
keycloak.realm("my-realm").clients().create(client);You can model agents as service accounts with client credentials grants. I've seen teams do it and it works fine for basic cases. But there's no delegation, no agent-specific audit, no MCP support. And you can't run it on edge infrastructure because JVM.
The real barrier is complexity. Setting up Keycloak takes a week if you're doing it properly. Minimum 512MB RAM. Config happens through an admin UI, not code. For a team of three building an agent system, it felt like way too much.
If your company already runs Keycloak, use it. If you're starting from scratch, don't.
Supabase Auth
GoTrue under the hood, written in Go, deeply integrated with Supabase and PostgreSQL. Free tier gives you 50K MAU. If you're already on Supabase, auth comes for free.
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
const { data, error } = await supabase.auth.signUp({
email: "dev@example.com",
password: "secure-password-123",
});Row-level security is the interesting part. Policies on Postgres tables, enforced automatically. For data authorization this works well.
For agents, not so much. Auth is coupled to PostgreSQL, so if you're using SQLite or D1, you're importing a whole database engine just for auth. The service role key is all-or-nothing. RLS handles "can this user read this row" but not "can this agent run a deployment." No delegation, no MCP support.
Good if you're already on Supabase. Not worth adopting just for auth.
The short version
| Your situation | Use this |
|---|---|
| Regular web app, no agents | Better Auth |
| Agent infrastructure, MCP servers | KavachOS |
| Enterprise with Java/SSO already | Keycloak (service accounts) |
| Already on Supabase | Supabase Auth + KavachOS |
| Need MCP OAuth 2.1 | KavachOS (only option right now) |
The tradeoff with KavachOS is a smaller community and docs that are still catching up. If that's a dealbreaker, you'll end up building agent auth on top of Better Auth or Keycloak yourself. Most teams are doing that right now, honestly.
Agent auth tooling is early. Most of the industry is duct-taping human auth onto agent systems and hoping nobody asks hard questions. That works for a while.
Share this post
Get started
Try kavachOS Cloud free
Free up to 1,000 MAU. Agent identity, MCP OAuth, and audit trail on every plan.