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
examples/agent/main.go for a complete, runnable end-to-end program.
Approval flow
Approval reusesoauth2’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:
- 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 afterBeginreturns{device_code, user_code}, callagentauth.PendingRequestStore.Create, which indexes the request by bothuser_codeand the agent’s principal ID. - Your own custom approve handler — mounted behind your normal
session-authentication middleware, same as
DeviceApproveHandlerwould be — resolves the agent’s principal ID viaPendingRequestStore.GetByUserCode(the human’s browser only ever suppliesuser_code, neverdevice_code) and callssvc.Authorize(ctx, userCode, agentPrincipalID)with that identity. - The agent polls the device-token endpoint once.
agentauth.NewAccessTokenIssuerlooks up the requested capabilities viaPendingRequestStore.GetByPrincipal(alloauth2.AccessTokenIssuer’s callback ever receives isprincipal, neverdevice_code), creates theGrant, 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.
