diff --git a/Index.md b/Index.md index 4768187..99b6d1e 100644 --- a/Index.md +++ b/Index.md @@ -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/) | | **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 | @@ -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 diff --git a/howto/auth.md b/howto/auth.md new file mode 100644 index 0000000..72e02d3 --- /dev/null +++ b/howto/auth.md @@ -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 ` 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: + +```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"}) +``` + +**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: + +```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") +} + +// 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 diff --git a/howto/interceptors.md b/howto/interceptors.md index df1a54e..a687f84 100644 --- a/howto/interceptors.md +++ b/howto/interceptors.md @@ -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/). + ## Adding custom interceptors to Default interceptors You can add your own interceptors to the [Default Interceptors] by appending to the list of interceptors. diff --git a/howto/production.md b/howto/production.md index bb40a24..f2382c9 100644 --- a/howto/production.md +++ b/howto/production.md @@ -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. diff --git a/howto/swagger.md b/howto/swagger.md index c5fb71a..eb12de4 100644 --- a/howto/swagger.md +++ b/howto/swagger.md @@ -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(` +API Reference + + +
+ +`)) + }) +} +``` + +**[RapiDoc](https://github.com/rapi-doc/RapiDoc)** — Web component that renders OpenAPI specs. Single `` tag: + +```go +w.Write([]byte(` + + + + +`)) +``` + +**[Redocly](https://github.com/Redocly/redoc)** — Three-panel reference docs (read-only, no "Try It"): + +```go +w.Write([]byte(` + + + +
+ +`)) +``` + +{: .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 diff --git a/tests/content.spec.ts b/tests/content.spec.ts index 851cb00..71d32e1 100644 --- a/tests/content.spec.ts +++ b/tests/content.spec.ts @@ -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"); + }); }); test.describe("Tables", () => { diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts index 711ae84..f1fb1b4 100644 --- a/tests/navigation.spec.ts +++ b/tests/navigation.spec.ts @@ -28,6 +28,7 @@ const howtoPages = [ "/howto/testing/", "/howto/workers/", "/howto/private-modules/", + "/howto/auth/", ]; test.describe("Page Loading", () => {