Table of contents

  1. Overview
  2. JWT authentication
    1. Enabling
    2. Testing JWT auth
    3. Accessing claims in handlers
    4. Using RSA or ECDSA keys
  3. API key authentication
    1. Enabling
    2. Sending API keys from clients
  4. Skipping auth for health checks
  5. Authorization
  6. Further reading

Overview

ColdBrew does not enforce a specific authentication mechanism, but the cookiecutter template includes ready-to-use examples for JWT and API key authentication built on top of go-grpc-middleware/v2 auth.

Auth is config-controlled — the interceptors are always wired in your generated project via service/auth/auth.go. To enable authentication, just set the corresponding environment variable. No code changes needed.

The auth interceptors run first in the ColdBrew interceptor chain — before timeout, rate limiting, logging, and metrics. Unauthenticated requests are rejected immediately without consuming rate limit tokens or generating response time logs.

JWT authentication

The JWT example uses golang-jwt/jwt/v5 — the most widely used Go JWT library — with HMAC-SHA256. It extracts the token from the Authorization: Bearer <token> gRPC metadata header. The library supports all standard signing algorithms (HMAC, RSA, ECDSA, EdDSA) and handles claims validation (expiry, not-before, issuer) out of the box.

Enabling

Set the JWT_SECRET environment variable:

env:
  - name: JWT_SECRET
    valueFrom:
      secretKeyRef:
        name: my-service-secrets
        key: jwt-secret

That’s it — the auth interceptors are registered automatically when the env var is set.

Testing JWT auth

The auth package includes a GenerateTestToken helper for local development:

import (
    "time"
    "your-module/service/auth"
)

token, err := auth.GenerateTestToken("a-string-secret-at-least-256-bits-long", "test-user", 1*time.Hour)

HTTP (via grpc-gateway):

# Generate a token (requires jwt-cli: brew install mike-engel/jwt-cli/jwt-cli)
TOKEN=$(jwt encode --secret "a-string-secret-at-least-256-bits-long" --sub "test-user" --exp "+1h")

# Call the service
curl -H "Authorization: Bearer $TOKEN" http://localhost:9091/api/v1/example/echo -d '{"msg":"hello"}'

gRPC (Go):

token, _ := auth.GenerateTestToken(os.Getenv("JWT_SECRET"), "test-user", 1*time.Hour)
md := metadata.Pairs("authorization", "Bearer "+token)
ctx := metadata.NewOutgoingContext(context.Background(), md)
resp, err := client.Echo(ctx, &pb.EchoRequest{Msg: "hello"})

grpcurl:

TOKEN=$(jwt encode --secret "a-string-secret-at-least-256-bits-long" --sub "test-user" --exp "+1h")
grpcurl -plaintext -H "Authorization: Bearer $TOKEN" \
  -d '{"msg":"hello"}' localhost:9090 com.github.ankurs.MySvc/Echo

Accessing claims in handlers

The JWT interceptor puts parsed claims into the request context. Access them with auth.ClaimsFromContext:

import "your-module/service/auth"

func (s *svc) MyMethod(ctx context.Context, req *pb.MyRequest) (*pb.MyResponse, error) {
    claims := auth.ClaimsFromContext(ctx)
    if claims == nil {
        // Should not happen — interceptor rejects unauthenticated requests
        return nil, status.Error(codes.Internal, "missing claims")
    }
    log.Info(ctx, "msg", "request from", "subject", claims.Subject)
    // ...
}

Using RSA or ECDSA keys

The default uses HMAC-SHA256 (symmetric) — faster and simpler, ideal for internal service-to-service auth where both sides share the secret. Use asymmetric keys (RSA, ECDSA) when tokens are issued by an external identity provider (Auth0, Keycloak, Google) where you only have the public key.

To switch, modify JWTAuthFunc in service/auth/auth.go — change the keyFunc to return your public key and update the WithValidMethods list. See the golang-jwt/jwt documentation for:

API key authentication

The API key example validates keys from the x-api-key gRPC metadata header against a configured set of valid keys.

Enabling

Set the API_KEYS environment variable (comma-separated list):

env:
  - name: API_KEYS
    valueFrom:
      secretKeyRef:
        name: my-service-secrets
        key: api-keys

That’s it — the auth interceptors are registered automatically when the env var is set.

Sending API keys from clients

gRPC (Go):

md := metadata.Pairs("x-api-key", "my-api-key")
ctx := metadata.NewOutgoingContext(context.Background(), md)
resp, err := client.MyMethod(ctx, req)

HTTP (via grpc-gateway):

curl -H "x-api-key: my-api-key" http://localhost:9091/api/v1/my-endpoint

For HTTP requests via grpc-gateway, ensure x-api-key is included in HTTP_HEADER_PREFIXES so it is forwarded as gRPC metadata. Add x-api-key to the config: HTTP_HEADER_PREFIXES=x-api-key.

Skipping auth for health checks

The cookiecutter template already skips auth for health checks, readiness checks, and gRPC reflection by default (via defaultSkipMethods in service/auth/auth.go). The override below is only needed if you want custom per-method skip logic.

To skip authentication for additional methods, your service can implement the ServiceAuthFuncOverride interface from go-grpc-middleware:

import grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"

// AuthFuncOverride replaces the global auth interceptor for this service.
// When implemented, the global AuthFunc is NOT called — this method is
// responsible for all auth decisions for this service's RPCs.
func (s *svc) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
    // Skip auth for specific methods
    switch fullMethodName {
    case "/mypackage.MySvc/PublicEndpoint":
        return ctx, nil
    }
    // For all other methods, delegate to the same auth function used globally.
    // Example with JWT:
    //   return auth.JWTAuthFunc(os.Getenv("JWT_SECRET"))(ctx)
    // Example with API key:
    //   return auth.APIKeyAuthFunc(strings.Split(os.Getenv("API_KEYS"), ","))(ctx)
    return ctx, status.Error(codes.Unauthenticated, "authentication required")
}

// Compile-time check
var _ grpcauth.ServiceAuthFuncOverride = (*svc)(nil)

Authorization

Authentication answers “who are you?” — authorization answers “what can you do?”. ColdBrew does not provide a built-in authorization framework, but gRPC-Go has native support for policy-based authorization:

  • grpc-go/authz — CEL-based policy engine built into gRPC-Go. Define allow/deny rules as JSON policies, evaluated per-RPC. Supports matching on method names, metadata headers, and authenticated identity.

For most services, a simple per-method check in your handler (using claims from the auth interceptor) is sufficient. Use grpc-go/authz when you need externalized, policy-driven access control.

Further reading