Before you start
- A Go HTTP server you can add a module to.
- A secrets manager or environment that can hold a 32 byte encryption key.
- Users who sign in through theAuth, so the agent has someone to act for.
Two tokens, two questions
The agent gets two kinds of token. One answers who the agent is: it authenticates as itself with client credentials, and the token's subject is the agent. The other answers who it is acting for: it exchanges a user's token for a delegated one, where the user is the subject and the agent is named as the actor. Your API can then allow, log and revoke each on its own.
Set it up
Step 1Add the module
theAuth runs inside your process as an
http.Handlerand keeps users, sessions and tokens in your own database.bash go get github.com/glincker/theauth-go/v2Step 2Turn on the authorization server and agent identity
Agent identity needs the authorization server, which needs a 32 byte
EncryptionKey. Declare each API the agent may call as a protected resource with its scopes. The memory store is for trying it out; use SQLite, Postgres or MySQL for real data.auth.go a, err := theauth.New(theauth.Config{ Storage: memory.New(), BaseURL: "https://as.example.com", EncryptionKey: loadEncryptionKey(), // 32 bytes from your secrets manager AuthorizationServer: &theauth.AuthorizationServerConfig{ Issuer: "https://as.example.com", Resources: []theauth.ProtectedResource{ {Identifier: "https://api.example.com", Scopes: []string{"read", "write"}}, }, }, AgentIdentity: &theauth.AgentConfig{}, }) if err != nil { log.Fatal(err) } defer a.Close()Step 3Register the agent for one user
RegisterAgentcreates the agent owned by a signed in user. BecauseResourceis set, the same call records a delegation grant from that user to the agent, limited to the scope you pass.go reg, err := a.RegisterAgent(ctx, theauth.RegisterAgentInput{ OwnerID: user.ID, Name: "report-bot", Scope: []string{"read"}, Resource: "https://api.example.com", }) if err != nil { return err } // reg.Secret.ClientID and reg.Secret.Secret are the agent's credential.Step 4Let the agent authenticate as itself
The agent uses the client credentials grant. The access token it gets has the agent as its subject, so your API knows a machine is calling.
go self, err := a.ClientCredentialsToken(ctx, theauth.TokenRequest{ GrantType: "client_credentials", ClientID: reg.Secret.ClientID, ClientSecret: reg.Secret.Secret, Resource: "https://api.example.com", Scope: []string{"read"}, })Step 5Act for the user through token exchange
To act for the user, the agent trades the user's access token for a delegated one (RFC 8693). The result names the user as the subject and the agent as the actor, and it never carries more scope than the grant allows.
go delegated, err := a.ExchangeToken(ctx, theauth.TokenExchangeRequest{ ClientID: reg.Secret.ClientID, ClientSecret: reg.Secret.Secret, SubjectToken: userAccessToken, SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token", Resource: "https://api.example.com", Scope: []string{"read"}, })Step 6Revoke access when the job is done
Use
RevokeDelegationto end the grant for one user,SuspendAgentto pause the agent, orRevokeAgentto retire it. Resource servers see the change on their next token introspection, and the user's own session is untouched.
Handle the secret once
- Keep the encryption key out of source control and out of logs. Load it at start up.
- Give each agent the smallest scope that does its job. A reporting agent needs
read, notwrite. - Register one agent per job, so revoking one does not stop the others.