-
Notifications
You must be signed in to change notification settings - Fork 0
docs: authentication howto page #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
523a16b
docs: add authentication howto page
ankurs 35c321c
fix: address PR review comments
ankurs df0919a
docs: add JWT testing examples (curl, gRPC, grpcurl, GenerateTestToken)
ankurs 5000e71
docs: add alternative OpenAPI UI implementations (Scalar, RapiDoc, Re…
ankurs 33a1082
fix: address PR review comments (round 3)
ankurs d229c08
fix: clarify AuthFuncOverride replaces global auth, add delegation ex…
ankurs 17b007a
fix: use Playwright locator assertions instead of textContent()
ankurs 4560d3e
fix: capitalize Bearer in gRPC example, return ctx not nil in AuthFun…
ankurs 673031a
fix: add missing time import, use context.Background() in snippets
ankurs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
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"}) | ||
|
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: | ||
|
|
||
|
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") | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.