Skip to content
2 changes: 2 additions & 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 Expand Up @@ -200,3 +201,4 @@ ColdBrew composes proven Go libraries — not replacements:
[Grafana]: https://grafana.com/
[promauto]: https://pkg.go.dev/github.com/prometheus/client_golang/prometheus/promauto
[ghz]: https://ghz.sh/
[ColdBrew cookiecutter]: /getting-started
189 changes: 189 additions & 0 deletions howto/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
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 }
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](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.

### Testing JWT auth

The auth package includes a `GenerateTestToken` helper for local development:

```go
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):**
```bash
# 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):**
```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"})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

**grpcurl:**
```bash
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`:

```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(context.Background(), md)
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

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

Comment thread
ankurs marked this conversation as resolved.
```go
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")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
60 changes: 60 additions & 0 deletions howto/swagger.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,66 @@ func main() {
}
```

### Alternative UI implementations

`SetOpenAPIHandler` accepts any `http.Handler`, so you can swap Swagger UI for any OpenAPI-compatible UI. ColdBrew mounts it at `SWAGGER_URL` (default `/swagger/`) with `http.StripPrefix`, so your handler receives requests with the prefix stripped.

{: .note .note-info }
The [cookiecutter template][ColdBrew cookiecutter] uses [swaggest/swgui](https://github.com/swaggest/swgui) (Swagger UI v5 embedded as a Go package). Update the UI version with `go get -u github.com/swaggest/swgui`.

**[Scalar](https://github.com/scalar/scalar)** — Modern API reference UI with dark/light themes and interactive "Try It" console. Load via CDN script tag:

```go
import (
"net/http"
openapi "your-module/third_party/OpenAPI" // provides SpecFS (embed.FS with *.json)
)

func scalarHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/spec.json" {
http.FileServerFS(openapi.SpecFS).ServeHTTP(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html>
<html><head><title>API Reference</title>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.29"></script>
</head><body>
Comment thread
ankurs marked this conversation as resolved.
<div id="app"></div>
<script>Scalar.createApiReference(document.getElementById('app'),
{ url: '/swagger/spec.json', theme: 'default' })</script>
</body></html>`))
Comment thread
ankurs marked this conversation as resolved.
})
}
```

**[RapiDoc](https://github.com/rapi-doc/RapiDoc)** — Web component that renders OpenAPI specs. Single `<rapi-doc>` tag:

```go
w.Write([]byte(`<!DOCTYPE html>
<html><head>
<script src="https://cdn.jsdelivr.net/npm/rapidoc@9.3/dist/rapidoc-min.js"></script>
Comment thread
ankurs marked this conversation as resolved.
</head><body>
<rapi-doc spec-url="/swagger/spec.json" theme="dark"></rapi-doc>
</body></html>`))
```

**[Redocly](https://github.com/Redocly/redoc)** — Three-panel reference docs (read-only, no "Try It"):

```go
w.Write([]byte(`<!DOCTYPE html>
<html><head>
<script src="https://cdn.redoc.ly/redoc/v2.4/bundles/redoc.standalone.js"></script>
</head><body>
<div id="redoc"></div>
<script>Redoc.init('/swagger/spec.json', {}, document.getElementById('redoc'))</script>
</body></html>`))
Comment thread
ankurs marked this conversation as resolved.
```

{: .important }
Pin CDN script versions in production to avoid unexpected breaking changes. For self-hosting, download the scripts and serve from your own assets.

---
[grpc-gateway's Swagger / Open API specification]: https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/adding_annotations/
[Config]: https://pkg.go.dev/github.com/go-coldbrew/core/config#Config
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 mainContent = page.locator("main, .main-content").first();
await expect(mainContent).toContainText("JWT");
await expect(mainContent).toContainText("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