Day 121 — Auth Interceptors¶
Month 5 · Week 2 · ⬅ Day 120 · Day 122 ➡ · Journal index
🎯 Learning Objective¶
Authenticate RPCs by extracting and validating a bearer token from incoming metadata, and inject the caller's identity into the context.
📚 Topics¶
metadata.MDandmetadata.FromIncomingContext- Case-insensitive header keys; per-RPC credentials on the client
- Returning
codes.Unauthenticated/codes.PermissionDenied
📖 Reading / Sources¶
📝 Notes¶
- gRPC carries per-call headers in
metadata.MD— amap[string][]string. On the server, read it withmd, ok := metadata.FromIncomingContext(ctx); missing metadata meansok == false→ [[metadata]]. - Metadata keys are case-insensitive and stored lower-cased (they become HTTP/2 headers). Reserved
grpc-*keys are for the runtime; binary values use a-binsuffix and are base64-encoded on the wire → [[http2-headers]]. - An auth interceptor does: read metadata →
parseBearer(md.Get("authorization"))→ validate token → on success return a new context carrying the identity (context.WithValue) so the handler can readwho am I→ [[auth-interceptor]] [[context]]. - Distinguish the two failure codes:
Unauthenticated= "I don't know who you are" (bad/missing token, HTTP 401);PermissionDenied= "I know you, but you can't do this" (HTTP 403) → [[status-codes]]. - Client side attaches the token with
metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+tok), or with per-RPC credentials (credentials.PerRPCCredentials) which also let gRPC require a secure transport before sending them. - Never put auth state in a package-level variable — it must ride the context so it's request-scoped and cancellation-safe. Use an unexported struct key type to avoid
context.WithValuecollisions → [[context-keys]]. - Auth needs TLS (Day 122): a bearer token over plaintext is a token anyone on the wire can replay.
💻 Code Examples¶
func AuthUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
vals := md.Get("authorization") // case-insensitive, returns []string
if len(vals) == 0 || !strings.HasPrefix(vals[0], "Bearer ") {
return nil, status.Error(codes.Unauthenticated, "missing bearer token")
}
user, err := verify(strings.TrimPrefix(vals[0], "Bearer "))
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
ctx = context.WithValue(ctx, userKey{}, user) // hand identity to the handler
return handler(ctx, req)
}
Stdlib rebuild (metadata in context + bearer auth):
examples/month-05/authmeta/main.go· Run:go run ./examples/month-05/authmeta
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Case-insensitive MD (Set/Append/Get) |
✅ | exercises/month-05/week-2/metadata |
ParseBearer with ErrNoToken sentinel |
✅ | exercises/month-05/week-2/metadata |
🐛 Mistakes Made¶
- Looked up
md["Authorization"]directly and got nothing — metadata keys are lower-cased. Usedmd.Get("authorization")(which lowercases for you). - Stored the user in a package var "for convenience" → it leaked across concurrent RPCs. Moved it into the context with a private key type.
❓ Open Questions¶
- For mTLS, should identity come from the client cert (peer info) instead of a token? When do you use both?
🧠 Active Recall (answer without looking)¶
- Q: What's the difference between
UnauthenticatedandPermissionDenied?
A
`Unauthenticated` (401) means the caller's identity is unknown or unverified — bad/missing credentials. `PermissionDenied` (403) means the identity is known but lacks permission for this operation.- Q: Why look up the authorization header with
md.Get("authorization")rather than indexing the map with the original casing?
A
gRPC metadata keys are case-insensitive and stored lower-cased (they map to HTTP/2 headers). `Get` lowercases the key for you; raw map indexing with mixed case misses.🪶 Feynman Reflection¶
The auth interceptor is a bouncer at the door. Every request carries an ID badge in its metadata (the bearer token). The bouncer reads the badge, checks it against the guest list, and either turns the request away with a 401, or stamps the request's context with "this is Alice" and waves it through to the handler.
🕳️ Knowledge Gaps¶
- JWT verification details (signature,
exp, audience) — today I treated the token as an opaque key.
✅ Summary¶
I can extract a bearer token from incoming metadata, validate it, return the right status code on failure, and pass identity to handlers via context.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 122: TLS & secure transport — the channel that makes tokens safe to send.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): metadata propagation & bearer auth interceptor (day 121)