Oct 23, 20265 min read

How to Add Role-Based Access Control to an Internal Tool Built by an AI Agent

An internal tool where every logged-in employee can do everything is one bad click away from an incident. Here is how to have an agent add roles from your identity provider to a Go backend, by hand and with Encore.

Login answers who someone is but not whether a support agent should be able to issue a $5,000 refund. Agents tend to stop at login unless asked, and when asked for roles they often add a check in the React component and leave the API open.

This guide adds roles to a Go backend the way NIST's RBAC model describes them: users get roles and roles carry permissions, which the server checks.

RBAC at a glance

QuestionWith EncorePlain Go middleware
Where roles come fromToken claims, parsed once in the auth handlerToken claims, parsed in middleware
How endpoints get themauth.Data() returns the typed user dataRequest context, cast by hand
Unauthenticated requestsRejected by the gateway for auth endpointsRejected if the middleware is mounted
Testing roles locallyencore run and encore test with local PostgresYour own test harness

Mapping groups to roles

Configure the identity provider to include a groups claim in the token, a private claim in JWT terms, then map group names to a short list of roles, such as viewer, support and finance. Keep the mapping in code so it is reviewed. The OWASP Authorization Cheat Sheet recommends denying by default and validating permissions on every request, which in practice means an unknown group maps to no role.

Plain Go: middleware and context

With net/http or chi, the agent writes middleware that verifies the token, puts a user struct in the request context, and a second wrapper that checks the role. It works, and it relies on every handler pulling the user out of the context with the right key and type. A missed wrapper on a new route gives everyone with a login access to it.

Encore: roles in the auth data

Encore

Encore combines an open source SDK for Go and TypeScript with a platform that provisions and deploys the app to your own AWS account. In Go, the auth handler returns a user ID and a struct of your choosing, and that struct can carry the roles:

package user import ( "context" "encore.dev/beta/auth" "encore.dev/beta/errs" ) type Data struct { Email string Roles []string } var groupRoles = map[string]string{ "support-team": "support", "finance-team": "finance", } //encore:authhandler func AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) { // verifyIDToken and its claims type are left out here. Implement them with an // OIDC library such as go-oidc: check signature, issuer and audience. claims, err := verifyIDToken(ctx, token) if err != nil { return "", nil, &errs.Error{Code: errs.Unauthenticated, Message: "invalid token"} } var roles []string for _, g := range claims.Groups { if r, ok := groupRoles[g]; ok { roles = append(roles, r) } } return auth.UID(claims.Subject), &Data{Email: claims.Email, Roles: roles}, nil }

An endpoint marked auth cannot run without a valid user, and a small helper in the service checks the role:

package refunds import ( "context" "slices" "encore.app/user" "encore.dev/beta/auth" "encore.dev/beta/errs" "encore.dev/storage/sqldb" ) var db = sqldb.NewDatabase("refunds", sqldb.DatabaseConfig{Migrations: "./migrations"}) func require(role string) error { d, _ := auth.Data().(*user.Data) if d == nil || !slices.Contains(d.Roles, role) { return &errs.Error{Code: errs.PermissionDenied, Message: "missing role " + role} } return nil } //encore:api auth method=POST path=/refunds/:id/approve func Approve(ctx context.Context, id int64) error { if err := require("finance"); err != nil { return err } res, err := db.Exec(ctx, `UPDATE refunds SET status = 'approved' WHERE id = $1 AND status = 'pending'`, id) if err != nil { return err } if res.RowsAffected() == 0 { return &errs.Error{Code: errs.NotFound, Message: "no pending refund with that id"} } return nil }

Encore returns structured errors such as PermissionDenied as the matching HTTP status. The agent can write tests that set the caller with et.OverrideAuthInfo from encore.dev/et and call Approve as each role with encore test, which runs against a local database the same way encore run does. Because the login check, the role check and the database live in the same app, an agent that adds an endpoint adds its check in that change and can run it as a support user before anyone reviews it.

The app deploys to your AWS account on push, with cloud access held by Encore's IAM role, not the agent. App roles are a separate thing from the Encore platform's own Admin, Member and Viewer roles, which govern who can deploy and change the app.

Good to know

verifyIDToken above is your OIDC verification, for example with github.com/coreos/go-oidc. Encore does not map identity provider groups for you. If roles change often, store them in a table keyed by user and look them up in the auth handler instead of reading claims.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

How to choose

  • Encore when you want the role data typed and available on every authenticated endpoint, and tests that run against a local database.
  • Plain Go middleware when the tool is part of an existing Go service with its own router and auth stack.

Frequently asked questions

How do I add role-based access control to an internal tool with an AI agent?

Have your identity provider put group membership in the token, map groups to roles in the backend, and check the role on the server before each sensitive action. With Encore in Go, the agent returns the roles from an auth handler as part of the user data, and endpoints read them with auth.Data(). It can test each role locally with encore run, which starts Postgres for the app.

Can I enforce roles in the frontend only?

No. Hiding a button is a usability feature. The backend has to reject the request, because anyone with a valid login can call the API directly.

Where should the role checks live?

In one place the agent reuses, such as a helper that takes the required role, and in tests that call each endpoint as each role. That makes it easy to see when a new endpoint is missing a check.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders