Before you start
- A Go server running theAuth with users who sign in. Give an AI agent its own credential covers the first setup and the token exchange.
- A 32 byte encryption key in your secrets manager.
- A storage backend with agent support: memory for trying it out, Postgres or MySQL, or a recent SQLite store.
Four ways to stop an agent
Rotating the secret cuts off whoever holds the old one. Suspending pauses the agent and can be undone. Revoking a delegation ends what it may do for one user. Revoking the agent is final. Each takes effect at your API on the next token introspection, and none of them touches the user's own session.
Register it
Step 1Turn on agent identity
Agents need the authorization server and its encryption key. An empty
AgentConfigtakes the defaults: a chain depth of 3, delegations capped at 90 days, and delegated tokens that live 15 minutes.auth.go a, err := theauth.New(theauth.Config{ Storage: store, BaseURL: "https://as.example.com", EncryptionKey: loadEncryptionKey(), AuthorizationServer: &theauth.AuthorizationServerConfig{ Issuer: "https://as.example.com", Resources: []theauth.ProtectedResource{ {Identifier: "https://api.example.com", Scopes: []string{"read", "write"}}, }, }, AgentIdentity: &theauth.AgentConfig{}, })Step 2Register the agent with a delegation that ends
RegisterAgentcreates the agent for a signed in user and, becauseResourceis set, the delegation grant that lets it act for them.GrantExpiresAtends the grant on a date. Keepreg.Agent.IDandreg.Grant.ID; the secret is shown once and only its hash is stored.go end := time.Now().Add(30 * 24 * time.Hour) reg, err := a.RegisterAgent(ctx, theauth.RegisterAgentInput{ OwnerID: user.ID, Name: "q4-report-bot", Scope: []string{"read"}, Resource: "https://api.example.com", GrantExpiresAt: &end, }) if err != nil { return err } // reg.Secret.ClientID and reg.Secret.Secret go straight to the agent's secret store.Step 3Give the agent itself an end date
An expired agent is inactive everywhere, and no token minted for it outlives it. Move the date, or clear it with
nil. It must be in the future and at most 10 years ahead.go err = a.SetAgentExpiry(ctx, reg.Agent.ID, &end)Step 4See who is calling at your API
With
mcpresourceat the resource server, a token the agent got for itself has the subjectagent:<id>. A delegated token has the user as the subject and the agent as the actor, so you can allow, log and stop each on its own.go v := mcpresource.New("https://api.example.com", mcpresource.WithJWKS("https://as.example.com/oauth/jwks"), mcpresource.WithIntrospection("https://as.example.com/oauth/introspect", clientID, clientSecret), ) handler := v.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, _ := v.Principal(r.Context()) log.Printf("subject=%s actor=%s chain=%v", p.Subject, p.Actor, p.ActorChain) }))
Stop it
Step 5Rotate the secret on a suspected leak
RotateAgentSecretmints a new secret and revokes every earlier credential of the agent. UseMintAgentCredentialinstead when you want an old and a new secret to overlap during a planned change.go secret, err := a.RotateAgentSecret(ctx, reg.Agent.ID) // secret.Secret is shown once; replace it in the agent's secret store.Step 6Pause the agent
SuspendAgentstops every token that mentions the agent on its next introspection.ResumeAgentbrings it back.go err = a.SuspendAgent(ctx, reg.Agent.ID, "investigating unexpected writes") err = a.ResumeAgent(ctx, reg.Agent.ID)Step 7End what it may do for one user
RevokeDelegationends one grant and leaves the agent and its other grants alone. A later token exchange under that grant is refused. WithConfig.AccountUXturned on, users can see and revoke their own delegations under/account/delegations.go err = a.RevokeDelegation(ctx, reg.Grant.ID, "user removed the integration")Step 8Retire it for good
RevokeAgentis final: a revoked agent cannot be resumed. Register a new agent if the job comes back.go err = a.RevokeAgent(ctx, reg.Agent.ID, "job finished")Step 9Confirm the token is dead
Ask the introspection endpoint about a token the agent held, with a confidential client's credentials. After a suspend, a revoke or an expiry, the answer is
{"active":false}even if the token has not reached its expiry time.bash curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "token=$AGENT_TOKEN" \ https://as.example.com/oauth/introspect