Custom Authorizer (RBAC)
Authentication answers who. Authorization answers may they do
this? — a separate decision the runtime asks of your Authorizer
after the principal has been resolved
(core / interfaces).
The runtime ships one trivial authorizer (security.Authorized() —
always-allow). Anything more interesting you write yourself.
A role-based authorizer
type principal struct {
Subject string
Roles []string
}
// roleACL: which roles may call which "method path".
var roleACL = map[string]map[string]bool{
"GET /pets": {"reader": true, "admin": true},
"POST /pets": {"writer": true, "admin": true},
"DELETE /pets/{id}": {"admin": true},
}
func RBACAuthorizer() runtime.Authorizer {
return runtime.AuthorizerFunc(func(r *http.Request, p any) error {
route := middleware.MatchedRouteFrom(r)
key := fmt.Sprintf("%s %s", r.Method, route.PathPattern)
allowed, ok := roleACL[key]
if !ok {
return errors.New(http.StatusForbidden, "no ACL entry for %s", key)
}
prin, ok := p.(*principal)
if !ok {
return errors.New(http.StatusForbidden, "principal type mismatch")
}
for _, role := range prin.Roles {
if allowed[role] {
return nil // 👍
}
}
return errors.New(http.StatusForbidden, "role %v cannot %s", prin.Roles, key)
})
}Full source: docs/examples/auth/customauthorizer/main.go
Two things worth knowing about the return value:
- A return implementing
errors.Erroris propagated as-is (status code preserved). - Any other error is wrapped as
errors.New(403, err.Error()).
That’s why the example uses errors.New(http.StatusForbidden, …)
rather than fmt.Errorf — to keep control of the status code.
Wire it
api := untyped.NewAPI(doc).WithJSONDefaults()
api.RegisterAuth("bearer", security.BearerAuth("bearer", verifyBearer))
api.RegisterAuthorizer(RBACAuthorizer())Full source: docs/examples/auth/customauthorizer/main.go
That’s it — the runtime calls Authorize on every authenticated
request after the authenticator has populated the principal.
Reading the principal & scopes elsewhere
Inside extra middleware mounted via
middleware.Builder,
or from a custom error handler:
principal := middleware.SecurityPrincipalFrom(r) // any
scopes := middleware.SecurityScopesFrom(r) // []string
route := middleware.MatchedRouteFrom(r) // *MatchedRouteFull source: docs/examples/auth/customauthorizer/main.go
Useful for audit logging, per-tenant rate limiting, or surfacing a “why was this denied?” message in error responses.
Variations
- OPA / Casbin / your own engine: same shape — call out to the
policy evaluator from inside the
AuthorizerFunc. - Skip authorization for some routes: combine the ACL with a
short-circuit on the matched route (
route.Operation.ID,route.PathPattern, etc.) before consulting the engine. - Per-method body inspection:
Authorizerruns after authentication but before parameter binding, so the request body has not been consumed at this point — for body-based decisions (“the document the user is editing must belong to them”), do the check inside the operation handler, where the bound params are available.