Skip to content
theAuth

Give an AI agent its own identity in Go, then revoke it

An agent with its own identity can be stopped without logging anyone out. This guide registers one with a delegation that ends, checks who is calling at the resource server, and walks through every way to stop it, from a secret rotation to a permanent revoke.

  • Practitioner
  • 3 min read
On this page

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

  1. Step 1Turn on agent identity

    Agents need the authorization server and its encryption key. An empty AgentConfig takes 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{},
    })
  2. Step 2Register the agent with a delegation that ends

    RegisterAgent creates the agent for a signed in user and, because Resource is set, the delegation grant that lets it act for them. GrantExpiresAt ends the grant on a date. Keep reg.Agent.ID and reg.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.
  3. 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)
  4. Step 4See who is calling at your API

    With mcpresource at the resource server, a token the agent got for itself has the subject agent:<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

  1. Step 5Rotate the secret on a suspected leak

    RotateAgentSecret mints a new secret and revokes every earlier credential of the agent. Use MintAgentCredential instead 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.
  2. Step 6Pause the agent

    SuspendAgent stops every token that mentions the agent on its next introspection. ResumeAgent brings it back.

    go
    err = a.SuspendAgent(ctx, reg.Agent.ID, "investigating unexpected writes")
    err = a.ResumeAgent(ctx, reg.Agent.ID)
  3. Step 7End what it may do for one user

    RevokeDelegation ends one grant and leaves the agent and its other grants alone. A later token exchange under that grant is refused. With Config.AccountUX turned on, users can see and revoke their own delegations under /account/delegations.

    go
    err = a.RevokeDelegation(ctx, reg.Grant.ID, "user removed the integration")
  4. Step 8Retire it for good

    RevokeAgent is 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")
  5. 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
Questions

Frequently asked questions

What is the difference between suspending and revoking an agent?

Suspending is reversible with ResumeAgent. Revoking is terminal, and a revoked agent cannot be resumed.

How long does a revocation take to reach my API?

Until the resource server's next introspection of the token, which is bounded by the introspection cache time you configure.

What happens when an agent expires?

It is treated as inactive everywhere. Client credentials and token exchange are refused, introspection reports its tokens inactive, and no token minted for it can last past the expiry.

Can a delegated token carry more scope than the grant?

No. The exchange consults the grant on every call, so scope outside the grant is refused and a revoked grant stops working.

Keep going
Next steps

Take it further with theAuth

The product page covers what it does, how to install it and where the docs live.