Skip to content

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.MD and metadata.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 — a map[string][]string. On the server, read it with md, ok := metadata.FromIncomingContext(ctx); missing metadata means ok == 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 -bin suffix 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 read who 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.WithValue collisions → [[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. Used md.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)

  1. Q: What's the difference between Unauthenticated and PermissionDenied?
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.
  1. Q: Why look up the authorization header with md.Get("authorization") rather than indexing the map with the original casing?
AgRPC 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)