Lesson 13 · Interceptors, middleware & auth
Auth & RBAC
Who are you (authn), and are you allowed (authz) — one interceptor, two jobs.
Your win: explain how our auth interceptor authenticates a caller from the JWT and authorizes them against per-method roles — and how internal service-to-service calls get through without an end-user token.
Two jobs, in order
1 · Authenticate — verify the JWT from metadata
The interceptor reads the token from incoming metadata (Lesson 10),
verifies the JWT (signature, issuer, audience), and injects the claims into context:
md, _ := metadata.FromIncomingContext(ctx)
token := md.Get("token") // Lesson 10
// verify against configured issuers…
ctx = interceptors.ContextWithJWTClaims(ctx, claims) // UserID, ResourcePath, roles
2 · Authorize — match roles against the per-method map
Each service declares a rbacDecider: full method path → allowed roles.
The interceptor fetches the caller's roles from Postgres and checks them:
var rbacDecider = map[string][]string{
"/spike.v1.EmailModifierService/ExternalSendEmail": {RoleSchoolAdmin, RoleHQStaff},
"/grpc.health.v1.Health/Watch": nil, // nil = any authenticated user
}
// groupDecider.Check: RetrieveUserRoles(ctx, repo, db) → match rbacDecider[fullMethod]
// no match → codes.PermissionDenied
rbacDecider or the server
panics at startup. nil means "any authenticated user" —
not "public" and not "omitted". You can't forget to protect an
endpoint; the build won't let you. (Repo rule: .claude/rules/security.md.)
Internal service-to-service: the fake JWT
Internal calls (like spike → notification's InternalRetrieveMedia, Lesson 1)
have no end-user token. They're listed in ignoreAuthEndpoint (skip JWT
verification) and fakeJwtCtxEndpoint, which injects a synthetic
school-admin claim carrying the resource_path (tenant) so downstream
RBAC and RLS still work (internal/golibs/interceptors/internal_api.go:32).
PermissionDenied as a
status (L8). And GroupFetcher is an interface (Go L6) so
role-fetching is swappable/testable. Every earlier lesson shows up here.
gRPC — Authentication guide
The official auth model: credentials, TLS, and token-based auth verified by a server-side interceptor — exactly our shape.
Check yourself (from memory)
Q1. Authentication asks "who are you?"; authorization asks…
rbacDecider.
Q2. Our per-method allowed roles live in…
map[fullMethod][]role in
cmd/server/*/auth.go; the interceptor matches the caller's roles against it.
Q3. An RPC missing from rbacDecider causes…
nil = any authenticated user, not "omitted".
token from incoming
metadata (L10), verify the JWT (signature/issuer/audience), inject claims (UserID,
ResourcePath, roles) into context. (2) AUTHORIZE — groupDecider.Check looks
up the full method path in the per-service rbacDecider, fetches the
caller's roles from Postgres (RetrieveUserRoles), and matches; no match →
codes.PermissionDenied. Every RPC must be in rbacDecider
(nil = any authenticated user) or the server panics at startup. Internal
calls skip JWT (ignoreAuthEndpoint) and get a synthetic school-admin claim
(fakeJwtCtxEndpoint) carrying resource_path.1. gRPC — Authentication; repo: repo-grpc-map.md (RBAC).