Agent Auth Plugin
@better-auth/agent-auth — first-class agent identity, capability grants, and approval flows for AI agents talking to your Quickback API.
@better-auth/agent-auth is the official server implementation of the Agent Auth Protocol. When you enable it, your Quickback worker becomes an Agent Auth provider: AI agents can register, request scoped capability grants from a real user, get approved through device-authorization or CIBA, and then call your API with short-lived signed JWTs.
The upstream @better-auth/agent-auth package is in heavy development and not yet stable. The Quickback integration pins a specific version and exposes a deliberately small surface for v1 — see "Limitations" at the bottom.
What gets enabled
Adding agentAuth to your auth config force-enables two more plugins:
| Plugin | Why |
|---|---|
bearer | Lets clients send Better Auth session tokens via Authorization: Bearer … instead of cookies. The agent surface is bearer-only. |
oauthProvider | OAuth 2.1 + OIDC provider so MCP clients can register, get tokens, and call your API as a real user. Replaces the deprecated mcp plugin. |
There's no useful Agent Auth deployment without these — the compiler auto-adds them so you don't trip over a confusing runtime error.
The full Bearer matrix on the agent surface (everything reached through /mcp and the agent-auth REST projection):
| Bearer flavor | Resolved by | Identity |
|---|---|---|
| BA session token | bearer plugin | The signed-in user |
| OAuth access token | oauthProvider plugin | The user who consented at OAuth time |
| Agent JWT | @better-auth/agent-auth | The agent (acting on behalf of its bound user) |
Cookie auth is not forwarded into /mcp — bearer is the only channel, and CSRF surface is zero.
Configuration
import { defineAuth, defineConfig, defineDatabase, defineRuntime } from "@quickback/compiler";
export default defineConfig({
name: "acme-api",
providers: {
runtime: defineRuntime("cloudflare"),
database: defineDatabase("cloudflare-d1"),
auth: defineAuth("better-auth", {
emailAndPassword: { enabled: true },
plugins: {
agentAuth: {
providerName: "Acme",
providerDescription: "Acme APIs for AI agents.",
modes: ["delegated", "autonomous"],
defaultHostCapabilities: ["GET", "HEAD"],
},
},
}),
},
});The plain-flag form is also valid — use it when you don't need to override any defaults:
auth: defineAuth("better-auth", {
plugins: ["agentAuth"],
})Options
| Option | Type | Default | Notes |
|---|---|---|---|
providerName | string | env.APP_NAME | Shown in the discovery doc and in the approval UI. |
providerDescription | string | "Agent-callable API powered by Quickback." | Free-form description in /.well-known/agent-configuration. |
modes | ("delegated" | "autonomous")[] | ["delegated", "autonomous"] | Supported agent modes. |
defaultHostCapabilities | string[] | true | ["GET", "HEAD"] | Capabilities auto-granted to a newly registered host. |
Most of the underlying plugin's surface (capability list, onExecute, custom approval-method resolver, onEvent audit hook) isn't projected through the compiler in v1 — see "Limitations".
Routes
When the plugin is enabled the compiler emits:
GET /.well-known/agent-configuration— discovery document with issuer, endpoints, supported modes, default location.- All of the plugin's own endpoints, mounted by Better Auth under
/auth/v1/agent/*.
The Account SPA gets a new route at /agents/approve for device-code approval. The plugin's deviceAuthorizationPage is automatically pinned at ${ACCOUNT_URL}/agents/approve.
Approval flow
- The agent calls
/auth/v1/agent/registerto create itself, then requests a capability grant. - The user enters the device code at
${ACCOUNT_URL}/agents/approve(or follows the deep-link the agent showed them). - The Account SPA fetches the pending request, shows the agent name + provider name + the capabilities being requested, with a "Step-up" badge on any capability whose
approvalStrengthis"webauthn". - User clicks Authorize (or Deny). Approval grants the capabilities; denial leaves the request pending.
- The agent polls or receives the token via the configured method, then calls capabilities with
Authorization: Bearer <agent-jwt>.
You revoke an agent at any time from your account settings (or directly via the BA admin API).
Schema
The plugin adds tables for agent, host, grant, and approval. They're created automatically by auth migrate / auth generate — no manual Drizzle work needed.
OpenAPI capability projection (default on)
When agentAuth is enabled, the compiler emits src/lib/openapi-spec.ts (the project's full OpenAPI spec inlined as a const) and wires createFromOpenAPI(openapiSpec, { ... }) into the agentAuth(...) call. Every operation with an operationId becomes a capability. No manual capability list required.
How requests flow:
- Agent calls
POST /capability/executewith its agent JWT. - The plugin verifies JWT + grant + constraints, then runs
onExecute. onExecute(the plugin's built-in proxy) makes a self-fetch to the worker's own URL (env.BETTER_AUTH_URL).resolveHeadersmints a Better Auth JWT withsub: agentSession.user.idso the inner request looks like an authenticated user call.- The worker's JWT fast-path picks it up, populates
ctx, and the full firewall + access + masking pipeline runs.
Default approval strength: GET → "session", POST/PUT/PATCH/DELETE → "webauthn". Override with the approvalStrength option.
To opt out and wire capabilities by hand:
auth: defineAuth("better-auth", {
plugins: {
agentAuth: { fromOpenAPI: false },
},
})When fromOpenAPI: false, the emitted call has capabilities: [] and no proxy — you write the capability list + onExecute in your own lib/auth.ts overrides.
Limitations (v1)
- WebAuthn step-up isn't wired in the approval UI yet. Capabilities tagged
approvalStrength: "webauthn"show a Step-up badge but the actual escalation call isn't made — the upstream API for that escalation is still settling. Until it lands, those capabilities will fail server-side regardless of approval. - No CLI helpers for listing / revoking agents. Use the BA admin API directly.
- Self-fetch on Cloudflare Workers adds one subrequest per capability call (~50ms). For typical agent traffic this is fine; for very high-frequency agents consider pinning a custom
onExecutethat usesapp.request()for in-process routing. @better-auth/agent-auth0.4.5 ships ESM-only exports without arequirecondition. The compiler image patches the package'spackage.jsonat install time so the Better Auth CLI'sauth generatestep (jiti / CJS) can resolve it. Remove the patch in the Dockerfile when upstream ships proper exports.
Why bearer-only?
The MCP and agent surfaces are server-to-server by nature. Cookies on those surfaces add CSRF surface for no benefit (no browser callers), and x-api-key was the legacy apiKey-plugin path that fragments the auth model. Bearer is the one true channel here. If your project still wants API-key auth, configure the apiKey plugin to accept its key via Authorization: Bearer so everything stays one channel.