The agentauth package implements capability-based authn/authz for AI/LLM agent clients, modeled on better-auth’s Agent Auth Protocol: an agent discovers capabilities, a human approves a scoped grant via the existing oauth2 device-authorization flow, and the agent then calls capabilities using a short-lived, replay-protected JWT. It reuses an existing go-security principal identity (an API key ID or OAuth2 client ID) as the agent’s identity — there is no separate agent-registration subsystem, and no new dependency beyond jwt and oauth2, both already in this repo.

Import

Quick setup

See examples/agent/main.go for a complete, runnable end-to-end program.

Approval flow

Approval reuses oauth2’s RFC 8628 device-authorization flow rather than a new mechanism — but not via oauth2.DeviceApproveHandler. That handler binds the device-flow principal to the approving human’s identity (core.AuthenticationFrom(ctx).Name()), not the agent’s — using it as-is would make every agent a given human approves collide on one grant. Instead:
  1. The agent presents its own existing credential (an API key ID or OAuth2 client ID) to your own wrapper around oauth2.DeviceAuthorizationService.Begin, along with the capabilities it wants. Right after Begin returns {device_code, user_code}, call agentauth.PendingRequestStore.Create, which indexes the request by both user_code and the agent’s principal ID.
  2. Your own custom approve handler — mounted behind your normal session-authentication middleware, same as DeviceApproveHandler would be — resolves the agent’s principal ID via PendingRequestStore.GetByUserCode (the human’s browser only ever supplies user_code, never device_code) and calls svc.Authorize(ctx, userCode, agentPrincipalID) with that identity.
  3. The agent polls the device-token endpoint once. agentauth.NewAccessTokenIssuer looks up the requested capabilities via PendingRequestStore.GetByPrincipal (all oauth2.AccessTokenIssuer’s callback ever receives is principal, never device_code), creates the Grant, and mints the first capability JWT.

Token refresh — there is no “poll again”

oauth2.DeviceCodeStore.Poll is single-use: a device flow’s approval mints exactly one token. A Grant is meant to be long-lived (default 24h) while capability JWTs stay short (recommend ~2 minutes), so agentauth provides RefreshCapabilityToken(ctx, principalID, grants, tokenCfg) — a plain function, not an HTTP handler — to mint a fresh token off a still-Active grant without a second device-flow approval. Call it from your own already-authenticated channel (a session/API-key-gated endpoint, a periodic job); agentauth never exposes it as a bare unauthenticated network endpoint, since whatever gates access to calling it is the only thing standing between an attacker and a fresh capability token.

Execute endpoint

ExecuteHandler is the authorization decision point: it validates the bearer JWT (signature, audience, issuer, expiry, replay), loads the caller’s Grant, and only proceeds if the requested capability is in both the JWT’s claimed capabilities and the stored grant’s — the intersection, not either alone. A capability Handler receives an AgentSession whose Capabilities field is exactly that intersection, never the raw grant, so a narrowly-scoped token can’t cause a Handler to believe the caller holds more than the token attests to. A Capability with a non-empty Location is never dispatched by ExecuteHandler — protect that URL yourself using ResolveSession, which does the same validation without checking a specific capability name.

Discovery and capability listing

DiscoveryHandler serves GET /.well-known/agent-configuration — provider metadata, the execute endpoint URL, and your existing device-flow approval endpoints (agentauth adds no new approval endpoints). CapabilitiesHandler serves GET /capability/list — every registered capability, to every authenticated, non-anonymous caller (checked via core.IsAuthenticated). There’s no per-agent filtering in v1; the execute endpoint’s grant intersection is the real authorization gate, and discovery is informational.

Key design deviation from better-auth’s Agent Auth Protocol

better-auth has the agent sign its own short-lived JWT with a key established at registration (proof-of-possession). Because agent registration is out of scope here, agentauth has the server mint the capability JWT and the agent simply presents it — weaker than agent-held-key signing (a stolen bearer JWT is usable by anyone holding it until jti/exp catch it), mitigated by a short TTL, the ReplayStore, and narrow aud/capabilities scoping.

Deployment constraint

MemoryGrantStore, MemoryReplayStore, and MemoryPendingRequestStore are process-local. Revocation and replay protection only hold within one process — deployments need a single instance or sticky routing until a shared (e.g. SQL-backed) store exists.

Not built (deferred)

CIBA approval (no existing primitive to adapt), an OpenAPI-to-capability adapter, an MCP-tool adapter, a SQL-backed grant/replay/pending store, per-agent capability-list filtering, and agent registration with per-agent signing keys (which would upgrade the deviation above to full protocol parity).