Skip to content
1 change: 1 addition & 0 deletions Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ A Kubernetes-native Go microservice framework for building production-grade gRPC
| **Prometheus Metrics** | Built-in request latency, error rate, and circuit breaker metrics at `/metrics` |
| **Error Tracking** | Stack traces, gRPC status codes, and async notification to [Sentry], Rollbar, or Airbrake |
| **Rate Limiting** | Per-pod token bucket rate limiter — disabled by default, pluggable via custom [`ratelimit.Limiter`](https://pkg.go.dev/github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/ratelimit#Limiter) interface for distributed or per-tenant rate limiting. Config: `RATE_LIMIT_PER_SECOND`. See [interceptors howto](/howto/interceptors#rate-limiting) |
| **Auth Examples** | JWT and API key authentication interceptor examples in the [cookiecutter template][ColdBrew cookiecutter], built on [go-grpc-middleware auth](https://github.com/grpc-ecosystem/go-grpc-middleware/tree/main/interceptors/auth). See [auth howto](/howto/auth/) |
Comment thread
ankurs marked this conversation as resolved.
| **Fast Serialization** | [vtprotobuf] codec enabled by default — faster gRPC marshalling with automatic fallback to standard protobuf |
| **Kubernetes-native** | Health/ready probes, graceful SIGTERM shutdown, structured JSON logs, Prometheus metrics — all wired automatically |
| **Swagger / OpenAPI** | Interactive API docs auto-served at `/swagger/` from your protobuf definitions |
Expand Down
147 changes: 147 additions & 0 deletions howto/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
layout: default
title: "Authentication"
parent: "How To"
nav_order: 18
description: "Adding JWT and API key authentication to ColdBrew gRPC services using go-grpc-middleware auth interceptors"
---
## Table of contents
{: .no_toc .text-delta }

1. TOC
{:toc}

## Overview

ColdBrew does not enforce a specific authentication mechanism, but the [cookiecutter template][ColdBrew cookiecutter] includes ready-to-use examples for **JWT** and **API key** authentication built on top of [go-grpc-middleware/v2 auth](https://github.com/grpc-ecosystem/go-grpc-middleware/tree/main/interceptors/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.

{: .note .note-info }
User-added interceptors run **first** in the ColdBrew interceptor chain — before timeout, rate limiting, logging, and metrics. This means authentication is enforced before any other processing.
Comment thread
ankurs marked this conversation as resolved.
Outdated

## JWT authentication

The JWT example uses [golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) — 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.
Comment thread
ankurs marked this conversation as resolved.

### Enabling

Set the `JWT_SECRET` environment variable:

```yaml
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.

### Accessing claims in handlers

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

```go
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](https://github.com/golang-jwt/jwt) for:

- [RSA parsing example](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-Parse-Rsa)
- [Custom claims structs](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-ParseWithClaims-CustomClaimsType)
- [JWKS key sets](https://github.com/MicahParks/keyfunc) for validating tokens from external identity providers (Auth0, Keycloak, etc.)

## 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):

```yaml
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):**
```go
md := metadata.Pairs("x-api-key", "my-api-key")
ctx := metadata.NewOutgoingContext(ctx, md)
Comment thread
ankurs marked this conversation as resolved.
Outdated
resp, err := client.MyMethod(ctx, req)
```

**HTTP (via grpc-gateway):**
```bash
curl -H "x-api-key: my-api-key" http://localhost:9091/api/v1/my-endpoint
```

{: .note .note-info }
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

By default, the auth interceptor applies to **all** RPCs including health and readiness checks. To skip authentication for specific methods, your service can implement the `ServiceAuthFuncOverride` interface from go-grpc-middleware:

Comment thread
ankurs marked this conversation as resolved.
```go
import (
grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"your-module/service/auth"
)

// AuthFuncOverride bypasses the global auth interceptor for specific methods.
func (s *svc) AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) {
// Skip auth for health and readiness checks
switch fullMethodName {
case "/grpc.health.v1.Health/Check",
"/grpc.health.v1.Health/Watch":
return ctx, nil
}
// Fall through to the global auth function for all other methods
return auth.JWTAuthFunc(s.jwtSecret)(ctx)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
ankurs marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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](https://github.com/grpc/grpc-go/tree/master/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

- [go-grpc-middleware/v2 auth](https://github.com/grpc-ecosystem/go-grpc-middleware/tree/main/interceptors/auth) — the `AuthFunc` pattern used by these examples
- [grpc-go/authz](https://github.com/grpc/grpc-go/tree/master/authz) — gRPC-native policy-based authorization
- [golang-jwt/jwt](https://github.com/golang-jwt/jwt) — the JWT library used in the example
- [Security hardening guide](/howto/production/#security-hardening) — TLS, admin port isolation, and other production security measures

[ColdBrew cookiecutter]: /getting-started
6 changes: 6 additions & 0 deletions howto/interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ For large-scale multi-service rate limiting, consider a dedicated rate limiting

Set `DISABLE_RATE_LIMIT=true` to remove the rate limiting interceptor from the chain entirely.

## Authentication

The [cookiecutter template][ColdBrew cookiecutter] includes ready-to-use JWT and API key authentication interceptors built on [go-grpc-middleware/v2 auth](https://github.com/grpc-ecosystem/go-grpc-middleware/tree/main/interceptors/auth). These interceptors are wired by default — set `JWT_SECRET` or `API_KEYS` environment variables to enable them.

For full documentation, see the [Authentication How-To](/howto/auth/).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Adding custom interceptors to Default interceptors

You can add your own interceptors to the [Default Interceptors] by appending to the list of interceptors.
Expand Down
2 changes: 1 addition & 1 deletion howto/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ Avoid adding PII (passwords, tokens, user data) to log context or error notifica
These are your responsibility to handle at the infrastructure level:

- **CORS** — ColdBrew does not handle CORS headers. Use a reverse proxy (Nginx, Envoy, Istio) or add CORS middleware to the HTTP gateway.
- **Authentication/authorization** — Admin endpoints (`/debug/pprof`, `/metrics`, `/swagger`) have no built-in auth. Disable them for public services or restrict access at the load balancer.
- **Authentication/authorization** — Admin endpoints (`/debug/pprof`, `/metrics`, `/swagger`) have no built-in auth. Disable them for public services or restrict access at the load balancer. For application-level auth (JWT, API keys), the [cookiecutter template][ColdBrew cookiecutter] includes ready-to-use examples — see [Authentication How-To](/howto/auth/).
- **Cluster-wide rate limiting** — Built-in rate limiting (`RATE_LIMIT_PER_SECOND`) is per-pod only. For cluster-wide or per-tenant rate limiting, use `interceptors.SetRateLimiter()` with a custom implementation or your load balancer. See [Interceptors How-To](/howto/interceptors#rate-limiting).
- **HTTP header forwarding** — `HTTP_HEADER_PREFIXES` forwards matching HTTP headers to gRPC metadata. Never add `authorization`, `cookie`, or `x-api-key` prefixes unless you are intentionally doing header-based gRPC auth.

Expand Down
9 changes: 9 additions & 0 deletions tests/content.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ test.describe("Code Blocks", () => {
const codeBlocks = page.locator("pre code");
expect(await codeBlocks.count()).toBeGreaterThanOrEqual(2);
});

test("auth howto renders code blocks", async ({ page }) => {
await page.goto("/howto/auth/");
const codeBlocks = page.locator("pre code");
expect(await codeBlocks.count()).toBeGreaterThanOrEqual(5);
const pageText = await page.locator("main, .main-content").first().textContent();
expect(pageText).toContain("JWT");
expect(pageText).toContain("API key");
});
Comment thread
ankurs marked this conversation as resolved.
});

test.describe("Tables", () => {
Expand Down
1 change: 1 addition & 0 deletions tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const howtoPages = [
"/howto/testing/",
"/howto/workers/",
"/howto/private-modules/",
"/howto/auth/",
];

test.describe("Page Loading", () => {
Expand Down
Loading