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

Authentication ≠ authorization Authentication answers "who are you?" — verify the caller's identity. Authorization answers "are you allowed to do this?" — check permissions for the specific method. Our single auth interceptor (Lessons 11–12) does both, authn first.1

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:

internal/usermgmt/pkg/interceptors/auth.go:120
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:

cmd/server/spike/auth.go:33 · internal/usermgmt/pkg/interceptors/auth.go:201,242
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
The rule that prevents accidents Every RPC must appear in 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).

Anchor — how it all connects This is one interceptor (L11) in the chain (L12) reading metadata (L10) to enforce access, returning PermissionDenied as a status (L8). And GroupFetcher is an interface (Go L6) so role-fetching is swappable/testable. Every earlier lesson shows up here.
Read this next

gRPC — Authentication guide

The official auth model: credentials, TLS, and token-based auth verified by a server-side interceptor — exactly our shape.

grpc.io/docs/guides/auth

Check yourself (from memory)

Q1. Authentication asks "who are you?"; authorization asks…

Authn = identity; authz = permission for this specific method. Our interceptor does authn first, then authz via 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…

Fail-closed by design — you can't forget to protect an endpoint. nil = any authenticated user, not "omitted".
Walk through how our auth interceptor authenticates and authorizes a call.
recall, then click to reveal
(1) AUTHENTICATE — read the 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.
🎓 That's Part 4 Interceptors, the chain, and auth/RBAC — the middleware that makes every RPC observed, bounded, and secured. Part 5 steps back to architecture: gRPC vs REST, load balancing, health/reflection, and the HTTP gateway.
Ready for the final Part 5 (architecture & production), or a mixed quiz across Lessons 11–13 first? Ask me.

1. gRPC — Authentication; repo: repo-grpc-map.md (RBAC).