diff --git a/CHANGELOG.md b/CHANGELOG.md index 11985b62..4143e5ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Grafana dashboard for adapter metrics (`charts/dashboards/hyperfleet-adapter.json`) — covers events processed, processing duration, errors by type, resource deletions, and adapter health ([HYPERFLEET-1360](https://issues.redhat.com/browse/HYPERFLEET-1360)) +- `clients.hyperfleet_api.auth.scheme` (Helm: `adapterConfig.hyperfleetApi.auth.scheme`) configures the Authorization header scheme sent with the service account token; defaults to `Bearer` for backwards compatibility, set to `ServiceAccount` when fronted by a gateway that differentiates human-jwt callers from machine callers ([HYPERFLEET-1480](https://issues.redhat.com/browse/HYPERFLEET-1480)) ### Fixed diff --git a/charts/README.md b/charts/README.md index e3f3b061..09e08a85 100644 --- a/charts/README.md +++ b/charts/README.md @@ -27,16 +27,17 @@ helm install hyperfleet-adapter oci://REGISTRY/hyperfleet-adapter \ | Key | Type | Default | Description | |-----|------|---------|-------------| -| adapterConfig | object | `{"create":true,"hyperfleetApi":{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"},"log":{"level":"info"}}` | Adapter deployment configuration. Controls how the adapter-config ConfigMap is created. Use `adapterConfig.yaml` for inline YAML, `adapterConfig.files` for chart-packaged files, or set `create: false` and provide `configMapName` to reference an existing ConfigMap. | +| adapterConfig | object | `{"create":true,"hyperfleetApi":{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"scheme":"Bearer","tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"},"log":{"level":"info"}}` | Adapter deployment configuration. Controls how the adapter-config ConfigMap is created. Use `adapterConfig.yaml` for inline YAML, `adapterConfig.files` for chart-packaged files, or set `create: false` and provide `configMapName` to reference an existing ConfigMap. | | adapterConfig.create | bool | `true` | Create the adapter-config ConfigMap | -| adapterConfig.hyperfleetApi | object | `{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"}` | HyperFleet API connection settings injected as environment variables | +| adapterConfig.hyperfleetApi | object | `{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"scheme":"Bearer","tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"}` | HyperFleet API connection settings injected as environment variables | | adapterConfig.hyperfleetApi.baseUrl | string | `"http://hyperfleet-api:8000"` | API base URL (`HYPERFLEET_API_BASE_URL`) | | adapterConfig.hyperfleetApi.version | string | `"v1"` | API version (`HYPERFLEET_API_VERSION`) | -| adapterConfig.hyperfleetApi.auth | object | `{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"}` | JWT bearer token authentication via Kubernetes projected ServiceAccount token | -| adapterConfig.hyperfleetApi.auth.enabled | bool | `false` | Enable bearer token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) | -| adapterConfig.hyperfleetApi.auth.audience | string | `"hyperfleet-api"` | ServiceAccount token audience (used for the projected volume) | +| adapterConfig.hyperfleetApi.auth | object | `{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"scheme":"Bearer","tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"}` | Token-based authentication via a Kubernetes projected service account token | +| adapterConfig.hyperfleetApi.auth.enabled | bool | `false` | Enable token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) | +| adapterConfig.hyperfleetApi.auth.audience | string | `"hyperfleet-api"` | Service account token audience (used for the projected volume) | | adapterConfig.hyperfleetApi.auth.tokenPath | string | `"/var/run/secrets/hyperfleet/token"` | Absolute path where the token file is mounted | -| adapterConfig.hyperfleetApi.auth.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected ServiceAccount token | +| adapterConfig.hyperfleetApi.auth.scheme | string | `"Bearer"` | Authorization header scheme used when sending the token (`HYPERFLEET_API_AUTH_SCHEME`). Defaults to `Bearer`; set to `ServiceAccount` when fronted by a gateway that differentiates machine callers from human-jwt callers. | +| adapterConfig.hyperfleetApi.auth.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected service account token | | adapterConfig.hyperfleetApi.auth.tokenCacheTtl | string | `"30s"` | How long the token is cached in memory (`HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL`). Zero means re-read on every request. | | adapterConfig.log | object | `{"level":"info"}` | Log level for the adapter | | adapterConfig.log.level | string | `"info"` | Log level (`debug`, `info`, `warn`, `error`) | diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml index 15f47994..e1871382 100644 --- a/charts/templates/deployment.yaml +++ b/charts/templates/deployment.yaml @@ -92,6 +92,8 @@ spec: {{- if .Values.adapterConfig.hyperfleetApi.auth.enabled }} - name: HYPERFLEET_API_AUTH_TOKEN_PATH value: {{ .Values.adapterConfig.hyperfleetApi.auth.tokenPath | quote }} + - name: HYPERFLEET_API_AUTH_SCHEME + value: {{ .Values.adapterConfig.hyperfleetApi.auth.scheme | quote }} - name: HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL value: {{ .Values.adapterConfig.hyperfleetApi.auth.tokenCacheTtl | quote }} {{- end }} diff --git a/charts/values.yaml b/charts/values.yaml index 87660efb..09cb8129 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -27,15 +27,20 @@ adapterConfig: baseUrl: http://hyperfleet-api:8000 # -- API version (`HYPERFLEET_API_VERSION`) version: v1 - # -- JWT bearer token authentication via Kubernetes projected ServiceAccount token + # -- Token-based authentication via a Kubernetes projected service account token auth: - # -- Enable bearer token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) + # -- Enable token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) enabled: false - # -- ServiceAccount token audience (used for the projected volume) + # -- Service account token audience (used for the projected volume) audience: hyperfleet-api # -- Absolute path where the token file is mounted tokenPath: /var/run/secrets/hyperfleet/token - # -- Token lifetime in seconds for the projected ServiceAccount token + # -- Authorization header scheme used when sending the token + # (`HYPERFLEET_API_AUTH_SCHEME`). Defaults to `Bearer`; set to + # `ServiceAccount` when fronted by a gateway that differentiates + # machine callers from human-jwt callers. + scheme: Bearer + # -- Token lifetime in seconds for the projected service account token expirationSeconds: 3600 # -- How long the token is cached in memory (`HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL`). # Zero means re-read on every request. diff --git a/cmd/adapter/main.go b/cmd/adapter/main.go index c35319bf..ad184d70 100644 --- a/cmd/adapter/main.go +++ b/cmd/adapter/main.go @@ -327,7 +327,7 @@ func createAPIClient(apiConfig configloader.HyperfleetAPIConfig) (hyperfleetapi. opts = append(opts, hyperfleetapi.WithDefaultHeader(key, value)) } - // Configure bearer token auth if set + // Configure token-based auth if set if apiConfig.Auth != nil { opts = append(opts, hyperfleetapi.WithAuth(apiConfig.Auth)) } diff --git a/configs/adapter-config-template.yaml b/configs/adapter-config-template.yaml index b84f5840..9befbfd2 100644 --- a/configs/adapter-config-template.yaml +++ b/configs/adapter-config-template.yaml @@ -114,12 +114,14 @@ clients: timeout: 2s retry_attempts: 3 retry_backoff: exponential - # Optional JWT bearer token authentication via a file (e.g. Kubernetes projected ServiceAccount token). - # When configured, the token is read from token_path and attached as Authorization: Bearer . - # token_path must be an absolute path. - # Environment variables: HYPERFLEET_API_AUTH_TOKEN_PATH, HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL + # Optional token-based authentication via a token file (e.g. Kubernetes projected service account token). + # When configured, the token is read from token_path and attached as Authorization: . + # scheme defaults to Bearer; set to ServiceAccount when fronted by a gateway that + # differentiates human-jwt callers from machine callers. token_path must be an absolute path. + # Environment variables: HYPERFLEET_API_AUTH_TOKEN_PATH, HYPERFLEET_API_AUTH_SCHEME, HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL # auth: # token_path: "/var/run/secrets/hyperfleet/token" + # scheme: Bearer # token_cache_ttl: "30s" # 0 = re-read on every request # Broker consumer configuration (adapter-level) diff --git a/docs/configuration.md b/docs/configuration.md index 90490858..746774e4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -64,6 +64,7 @@ clients: X-Example: "value" auth: token_path: "/var/run/secrets/hyperfleet/token" + scheme: "Bearer" token_cache_ttl: "30s" broker: subscription_id: "example-subscription" @@ -115,7 +116,8 @@ clients: - `base_delay` (duration string): Initial retry delay. Default: `1s`. - `max_delay` (duration string): Maximum retry delay. Default: `30s`. - `default_headers` (map[string]string): Headers added to all API requests. -- `auth.token_path` (string): Absolute path to a file containing a JWT bearer token. When set, the token is read from this file and attached as `Authorization: Bearer ` on every request. Typically a Kubernetes projected ServiceAccount token. Must be an absolute path. +- `auth.token_path` (string): Absolute path to a file containing a service account token. When set, the token is read from this file and attached as `Authorization: ` on every request. Typically a Kubernetes projected service account token. Must be an absolute path. +- `auth.scheme` (string): Authorization header scheme used when sending the token. Defaults to `Bearer`. Set to `ServiceAccount` when fronted by a gateway that differentiates human-jwt callers from machine callers. - `auth.token_cache_ttl` (duration string): How long the token is cached in memory before re-reading the file. Zero (default) means re-read on every request. ### Broker (`clients.broker`) @@ -293,6 +295,7 @@ All deployment overrides use the `HYPERFLEET_` prefix unless noted. - `HYPERFLEET_API_BASE_DELAY` -> `clients.hyperfleet_api.base_delay` - `HYPERFLEET_API_MAX_DELAY` -> `clients.hyperfleet_api.max_delay` - `HYPERFLEET_API_AUTH_TOKEN_PATH` -> `clients.hyperfleet_api.auth.token_path` +- `HYPERFLEET_API_AUTH_SCHEME` -> `clients.hyperfleet_api.auth.scheme` - `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` -> `clients.hyperfleet_api.auth.token_cache_ttl` **Broker** diff --git a/docs/deployment.md b/docs/deployment.md index 6ca3bc55..e9b8bed3 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -84,11 +84,12 @@ These fields have first-class Helm values that the chart injects as environment | `adapterConfig.log.level` | Log level (`debug`, `info`, `warn`, `error`) | `LOG_LEVEL` | `info` | | `adapterConfig.hyperfleetApi.baseUrl` | HyperFleet API base URL | `HYPERFLEET_API_BASE_URL` | `http://hyperfleet-api:8000` | | `adapterConfig.hyperfleetApi.version` | API version | `HYPERFLEET_API_VERSION` | `v1` | -| `adapterConfig.hyperfleetApi.auth.enabled` | Enable JWT bearer token auth | — (controls volume + env vars) | `false` | +| `adapterConfig.hyperfleetApi.auth.enabled` | Enable token auth | — (controls volume + env vars) | `false` | | `adapterConfig.hyperfleetApi.auth.tokenPath` | Absolute path to the token file | `HYPERFLEET_API_AUTH_TOKEN_PATH` | `/var/run/secrets/hyperfleet/token` | +| `adapterConfig.hyperfleetApi.auth.scheme` | Authorization header scheme sent with the token | `HYPERFLEET_API_AUTH_SCHEME` | `Bearer` | | `adapterConfig.hyperfleetApi.auth.tokenCacheTtl` | In-memory token cache TTL | `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` | `30s` | -| `adapterConfig.hyperfleetApi.auth.audience` | ServiceAccount token audience | — (used in projected volume) | `hyperfleet-api` | -| `adapterConfig.hyperfleetApi.auth.expirationSeconds` | ServiceAccount token lifetime (seconds) | — (used in projected volume) | `3600` | +| `adapterConfig.hyperfleetApi.auth.audience` | Service account token audience | — (used in projected volume) | `hyperfleet-api` | +| `adapterConfig.hyperfleetApi.auth.expirationSeconds` | Service account token lifetime (seconds) | — (used in projected volume) | `3600` | ### Fields settable via the `env` list @@ -147,12 +148,12 @@ When using individual properties, `broker.type` must be set to `googlepubsub` or ## HyperFleet API Authentication -The adapter can authenticate to the HyperFleet API using a Kubernetes projected ServiceAccount token (JWT bearer token). Authentication is **disabled by default** — existing deployments are unaffected. +The adapter can authenticate to the HyperFleet API using a Kubernetes projected service account token. Authentication is **disabled by default** — existing deployments are unaffected. When enabled, the Helm chart: 1. Mounts a projected `serviceAccountToken` volume at the configured `tokenPath` directory. -2. Sets `HYPERFLEET_API_AUTH_TOKEN_PATH` and `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` env vars. -3. The adapter reads the token file and attaches `Authorization: Bearer ` to every HyperFleet API request. +2. Sets `HYPERFLEET_API_AUTH_TOKEN_PATH`, `HYPERFLEET_API_AUTH_SCHEME`, and `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` env vars. +3. The adapter reads the token file and attaches `Authorization: ` to every HyperFleet API request. `scheme` defaults to `Bearer`; set it to `ServiceAccount` when fronted by a gateway that differentiates human-jwt callers from machine callers. ```yaml adapterConfig: @@ -161,6 +162,7 @@ adapterConfig: enabled: true audience: hyperfleet-api # token audience claimed by the API server tokenPath: /var/run/secrets/hyperfleet/token + scheme: Bearer # defaults to Bearer; use ServiceAccount behind a gateway that expects it expirationSeconds: 3600 # kubelet rotates the token before expiry tokenCacheTtl: 30s # re-read file every 30s; 0 = re-read per request ``` @@ -191,6 +193,7 @@ The chart automatically sets these environment variables from Helm values: | `HYPERFLEET_API_BASE_URL` | `adapterConfig.hyperfleetApi.baseUrl` | Always | | `HYPERFLEET_API_VERSION` | `adapterConfig.hyperfleetApi.version` | Always | | `HYPERFLEET_API_AUTH_TOKEN_PATH` | `adapterConfig.hyperfleetApi.auth.tokenPath` | When `auth.enabled` is `true` | +| `HYPERFLEET_API_AUTH_SCHEME` | `adapterConfig.hyperfleetApi.auth.scheme` | When `auth.enabled` is `true` | | `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` | `adapterConfig.hyperfleetApi.auth.tokenCacheTtl` | When `auth.enabled` is `true` | | `BROKER_CONFIG_FILE` | Hardcoded `/etc/broker/broker.yaml` | Always | | `HYPERFLEET_BROKER_SUBSCRIPTION_ID` | `broker.googlepubsub.subscriptionId` | When broker type is `googlepubsub` | diff --git a/internal/configloader/viper_loader.go b/internal/configloader/viper_loader.go index 15b25566..d356779a 100644 --- a/internal/configloader/viper_loader.go +++ b/internal/configloader/viper_loader.go @@ -43,6 +43,7 @@ var viperKeyMappings = map[string]string{ "clients::hyperfleet_api::base_delay": "API_BASE_DELAY", "clients::hyperfleet_api::max_delay": "API_MAX_DELAY", "clients::hyperfleet_api::auth::token_path": "API_AUTH_TOKEN_PATH", + "clients::hyperfleet_api::auth::scheme": "API_AUTH_SCHEME", "clients::hyperfleet_api::auth::token_cache_ttl": "API_AUTH_TOKEN_CACHE_TTL", "clients::broker::subscription_id": "BROKER_SUBSCRIPTION_ID", "clients::broker::topic": "BROKER_TOPIC", diff --git a/internal/hyperfleetapi/client.go b/internal/hyperfleetapi/client.go index 2a330b45..c2a8d84b 100644 --- a/internal/hyperfleetapi/client.go +++ b/internal/hyperfleetapi/client.go @@ -38,6 +38,7 @@ type httpClient struct { client *http.Client config *ClientConfig tokenSource *fileTokenSource + authScheme string } // ClientOption is a functional option for configuring the client @@ -111,7 +112,7 @@ func WithBaseURL(baseURL string) ClientOption { } } -// WithAuth configures JWT bearer token authentication from a file. +// WithAuth configures token-based authentication from a token file. func WithAuth(auth *AuthConfig) ClientOption { return func(c *httpClient) { c.config.Auth = auth @@ -157,9 +158,13 @@ func NewClient(opts ...ClientOption) (Client, error) { } } - // Initialize token source for bearer token auth if configured + // Initialize the token source if auth is configured if c.config.Auth != nil && c.config.Auth.TokenPath != "" { c.tokenSource = newFileTokenSource(c.config.Auth.TokenPath, c.config.Auth.TokenCacheTTL) + c.authScheme = c.config.Auth.Scheme + if c.authScheme == "" { + c.authScheme = DefaultAuthScheme + } } return c, nil @@ -334,13 +339,13 @@ func (c *httpClient) doRequest(ctx context.Context, req *Request) (*Response, er httpReq.Header.Set(k, v) } - // Inject bearer token auth header + // Inject the auth header if c.tokenSource != nil { tok, authErr := c.tokenSource.get() if authErr != nil { return nil, fmt.Errorf("getting auth token: %w", authErr) } - httpReq.Header.Set("Authorization", "Bearer "+tok) + httpReq.Header.Set("Authorization", c.authScheme+" "+tok) } // Set default Content-Type for requests with body diff --git a/internal/hyperfleetapi/client_test.go b/internal/hyperfleetapi/client_test.go index 9f072101..e7e055d3 100644 --- a/internal/hyperfleetapi/client_test.go +++ b/internal/hyperfleetapi/client_test.go @@ -208,7 +208,7 @@ func TestClientWithHeaders(t *testing.T) { defer server.Close() client, err := NewClient(WithBaseURL(server.URL), - WithDefaultHeader("Authorization", "Bearer default-token")) + WithDefaultHeader("Authorization", "ServiceAccount default-token")) require.NoError(t, err, "failed to create client") ctx := context.Background() @@ -218,8 +218,8 @@ func TestClientWithHeaders(t *testing.T) { ) require.NoError(t, err, "unexpected error") - if receivedAuth != "Bearer default-token" { - t.Errorf("expected Authorization header 'Bearer default-token', got %q", receivedAuth) + if receivedAuth != "ServiceAccount default-token" { + t.Errorf("expected Authorization header 'ServiceAccount default-token', got %q", receivedAuth) } if receivedCustom != "custom-value" { @@ -667,10 +667,10 @@ func TestAPIErrorInRetryExhausted(t *testing.T) { } } -func TestClientBearerTokenAuth(t *testing.T) { +func TestClientDefaultBearerScheme(t *testing.T) { dir := t.TempDir() tokenFile := filepath.Join(dir, "token") - if err := os.WriteFile(tokenFile, []byte("test-jwt-token"), 0600); err != nil { + if err := os.WriteFile(tokenFile, []byte("test-token"), 0600); err != nil { t.Fatal(err) } @@ -690,8 +690,36 @@ func TestClientBearerTokenAuth(t *testing.T) { _, err = client.Get(context.Background(), "/test") require.NoError(t, err) - if receivedAuth != "Bearer test-jwt-token" { - t.Errorf("Authorization = %q, want %q", receivedAuth, "Bearer test-jwt-token") + if receivedAuth != "Bearer test-token" { + t.Errorf("Authorization = %q, want %q", receivedAuth, "Bearer test-token") + } +} + +func TestClientConfiguredServiceAccountScheme(t *testing.T) { + dir := t.TempDir() + tokenFile := filepath.Join(dir, "token") + if err := os.WriteFile(tokenFile, []byte("test-token"), 0600); err != nil { + t.Fatal(err) + } + + var receivedAuth string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedAuth = r.Header.Get("Authorization") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client, err := NewClient( + WithBaseURL(server.URL), + WithAuth(&AuthConfig{TokenPath: tokenFile, Scheme: "ServiceAccount", TokenCacheTTL: 0}), + ) + require.NoError(t, err) + + _, err = client.Get(context.Background(), "/test") + require.NoError(t, err) + + if receivedAuth != "ServiceAccount test-token" { + t.Errorf("Authorization = %q, want %q", receivedAuth, "ServiceAccount test-token") } } diff --git a/internal/hyperfleetapi/token.go b/internal/hyperfleetapi/token.go index d2282421..257a6f20 100644 --- a/internal/hyperfleetapi/token.go +++ b/internal/hyperfleetapi/token.go @@ -8,7 +8,7 @@ import ( "time" ) -// fileTokenSource reads a bearer token from disk on every call, or caches it +// fileTokenSource reads a token from disk on every call, or caches it // for cacheTTL when cacheTTL > 0. A zero cacheTTL disables caching and causes // the file to be re-read on every request. It is safe for concurrent use. type fileTokenSource struct { diff --git a/internal/hyperfleetapi/types.go b/internal/hyperfleetapi/types.go index 1acd0f22..f1a80267 100644 --- a/internal/hyperfleetapi/types.go +++ b/internal/hyperfleetapi/types.go @@ -34,12 +34,20 @@ const ( // Client Configuration // ----------------------------------------------------------------------------- -// AuthConfig holds optional JWT bearer token authentication configuration. -// When set, a bearer token is read from TokenPath and injected as an -// Authorization header on every outbound request. +// DefaultAuthScheme is the Authorization header scheme used when AuthConfig.Scheme +// is not set, matching the "Bearer"-only validation in hyperfleet-api's JWT handler. +const DefaultAuthScheme = "Bearer" + +// AuthConfig holds optional token-based authentication configuration. +// When set, a token is read from TokenPath and injected using Scheme +// (defaults to "Bearer") on every outbound request. type AuthConfig struct { - // TokenPath is the absolute path to a file containing the bearer token. + // TokenPath is the absolute path to a file containing the service account token. TokenPath string `yaml:"token_path,omitempty" mapstructure:"token_path"` + // Scheme is the Authorization header scheme used when sending the token. + // Defaults to "Bearer" when empty; set to "ServiceAccount" when fronted by + // a gateway that differentiates human-jwt callers from machine callers. + Scheme string `yaml:"scheme,omitempty" mapstructure:"scheme"` // TokenCacheTTL controls how long the token is cached in memory. // Zero means the file is re-read on every request. TokenCacheTTL time.Duration `yaml:"token_cache_ttl,omitempty" mapstructure:"token_cache_ttl"` @@ -49,7 +57,7 @@ type AuthConfig struct { type ClientConfig struct { // DefaultHeaders are headers added to all requests DefaultHeaders map[string]string `yaml:"default_headers,omitempty" mapstructure:"default_headers"` - // Auth configures optional JWT bearer token authentication. + // Auth configures optional token-based authentication. // When nil, requests are sent without an Authorization header. Auth *AuthConfig `yaml:"auth,omitempty" mapstructure:"auth"` // BaseURL is the base URL for all API requests (must be set by caller)