Labels: ready-to-code
Depends on: Issue 11
Implement KeycloakTokenManager for service-account Kagenti authentication via OAuth2 Client Credentials Grant.
Approach Change (2026-06-26)
Previous approach: Per-user identity delegation via RFC 8693 token exchange (TokenExchangeManager). Each user's OIDC token was exchanged for a Kagenti-scoped token with per-user caching and graceful fallback.
Current approach: Service-account authentication via OAuth2 Client Credentials Grant (KeycloakTokenManager). A single service-account token is obtained from Keycloak, cached with expiry buffer, and used for all Kagenti requests. User identity is propagated via X-Backstage-User header for audit purposes (informational only, not for authentication).
Rationale: The service-account approach is simpler, proven in production, and sufficient for current requirements. Per-user token exchange adds complexity (auth proxy dependency, RFC 8693 exchange flow, per-user cache management) without proportional benefit at this stage. Service-account auth with user context headers provides adequate audit trails.
Tasks
From openspec/changes/security-safety-governance/tasks.md section 7:
- 7.1 Create
KeycloakTokenManager implementing OAuth2 Client Credentials Grant
- 7.2 Add token caching with configurable expiry buffer (default 60s)
- 7.3 Add concurrent token request deduplication (single in-flight Keycloak call)
- 7.4 Add
getTokenForStreaming(minLifetimeMs) for SSE connection support
- 7.5 Add single 401 retry with cache invalidation and fresh token fetch
- 7.6 Config schema fields already exist in
plugins/boost-backend/src/config/schemas.ts — no schema work needed
- 7.7 Integrate into
KagentiApiClient.requestCore() — add Authorization: Bearer and X-Backstage-User headers
Config Fields
The config fields are already defined in plugins/boost-backend/src/config/schemas.ts. The KeycloakTokenManager consumer must read them at startup:
| Key |
Default |
Description |
boost.kagenti.auth.tokenEndpoint |
— |
Keycloak token endpoint URL |
boost.kagenti.auth.clientId |
— |
OAuth2 client ID for service-account |
boost.kagenti.auth.clientSecret |
— |
OAuth2 client secret (visibility: secret) |
boost.kagenti.auth.tokenExpiryBufferSeconds |
60 |
Seconds before expiry to refresh token |
Implementation Constraints
These constraints are codified in the specs and must be followed:
-
Retry limit: On HTTP 401 from Kagenti, invalidate the cached token, fetch a fresh one, and retry the request at most once. If the retried request also returns 401, propagate the error to the caller. Do not loop. (See access-control/spec.md and design.md Decision 4.)
-
Consumer-applied default for tokenExpiryBufferSeconds: The Zod schema does NOT include .default(60) — raw config resolution bypasses Zod defaults. KeycloakTokenManager must apply its own fallback: const buffer = configValue ?? 60.
-
Mutual dependency validation: The three core auth fields (tokenEndpoint, clientId, clientSecret) are individually optional in the schema (because Keycloak auth itself is optional), but KeycloakTokenManager must validate at startup that all three are present together. If any are set but not all, throw a ConfigurationError.
-
No references to Augment or Citi in code, config keys, comments, or error messages. Boost is a clean-room reimplementation. The only Augment references allowed are in high-level specification context (e.g., "Augment lesson" in boost-context.md).
Reference Implementation
The augment workspace has a working KeycloakTokenManager that can be used as a reference for the token caching, streaming, and retry patterns:
workspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KeycloakTokenManager.ts
workspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KeycloakTokenManager.test.ts
workspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KagentiApiClient.ts
workspaces/augment/plugins/augment-backend/src/providers/kagenti/client/requestCore.ts
Important: Do not copy code from augment. Use it to understand the patterns (token caching with expiry buffer, concurrent deduplication via shared promise, getTokenForStreaming, 401 retry), then implement cleanly in boost's architecture.
Where to Put the Code
The boost Kagenti module already exists at plugins/boost-backend-module-kagenti/src/. Key existing files:
provider/KeycloakTokenCache.ts — existing cache wrapper using Backstage CacheService
provider/KagentiProvider.ts — the provider that will consume KeycloakTokenManager
types.ts — module-local type definitions
Create KeycloakTokenManager.ts (and .test.ts) in the provider/ directory, following the module's existing patterns.
Specifications
openspec/changes/security-safety-governance/specs/access-control/spec.md — Service-account auth scenarios (6 scenarios covering token acquisition, streaming, 401 retry, user identity propagation, config, LlamaStack unaffected)
openspec/changes/security-safety-governance/design.md — Decision 4 (service-account auth with token caching and streaming support)
openspec/changes/security-safety-governance/tasks.md — Section 7 (all tasks)
openspec/changes/platform-operations-deployment/specs/runtime-config/spec.md — Config field scenarios
specifications/boost-context.md — Design principles (read Principle 10 for auth context)
Labels:
ready-to-codeDepends on: Issue 11
Implement
KeycloakTokenManagerfor service-account Kagenti authentication via OAuth2 Client Credentials Grant.Approach Change (2026-06-26)
Previous approach: Per-user identity delegation via RFC 8693 token exchange (
TokenExchangeManager). Each user's OIDC token was exchanged for a Kagenti-scoped token with per-user caching and graceful fallback.Current approach: Service-account authentication via OAuth2 Client Credentials Grant (
KeycloakTokenManager). A single service-account token is obtained from Keycloak, cached with expiry buffer, and used for all Kagenti requests. User identity is propagated viaX-Backstage-Userheader for audit purposes (informational only, not for authentication).Rationale: The service-account approach is simpler, proven in production, and sufficient for current requirements. Per-user token exchange adds complexity (auth proxy dependency, RFC 8693 exchange flow, per-user cache management) without proportional benefit at this stage. Service-account auth with user context headers provides adequate audit trails.
Tasks
From
openspec/changes/security-safety-governance/tasks.mdsection 7:KeycloakTokenManagerimplementing OAuth2 Client Credentials GrantgetTokenForStreaming(minLifetimeMs)for SSE connection supportplugins/boost-backend/src/config/schemas.ts— no schema work neededKagentiApiClient.requestCore()— addAuthorization: BearerandX-Backstage-UserheadersConfig Fields
The config fields are already defined in
plugins/boost-backend/src/config/schemas.ts. TheKeycloakTokenManagerconsumer must read them at startup:boost.kagenti.auth.tokenEndpointboost.kagenti.auth.clientIdboost.kagenti.auth.clientSecretboost.kagenti.auth.tokenExpiryBufferSeconds60Implementation Constraints
These constraints are codified in the specs and must be followed:
Retry limit: On HTTP 401 from Kagenti, invalidate the cached token, fetch a fresh one, and retry the request at most once. If the retried request also returns 401, propagate the error to the caller. Do not loop. (See
access-control/spec.mdanddesign.mdDecision 4.)Consumer-applied default for
tokenExpiryBufferSeconds: The Zod schema does NOT include.default(60)— raw config resolution bypasses Zod defaults.KeycloakTokenManagermust apply its own fallback:const buffer = configValue ?? 60.Mutual dependency validation: The three core auth fields (
tokenEndpoint,clientId,clientSecret) are individually optional in the schema (because Keycloak auth itself is optional), butKeycloakTokenManagermust validate at startup that all three are present together. If any are set but not all, throw aConfigurationError.No references to Augment or Citi in code, config keys, comments, or error messages. Boost is a clean-room reimplementation. The only Augment references allowed are in high-level specification context (e.g., "Augment lesson" in
boost-context.md).Reference Implementation
The augment workspace has a working
KeycloakTokenManagerthat can be used as a reference for the token caching, streaming, and retry patterns:workspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KeycloakTokenManager.tsworkspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KeycloakTokenManager.test.tsworkspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KagentiApiClient.tsworkspaces/augment/plugins/augment-backend/src/providers/kagenti/client/requestCore.tsImportant: Do not copy code from augment. Use it to understand the patterns (token caching with expiry buffer, concurrent deduplication via shared promise,
getTokenForStreaming, 401 retry), then implement cleanly in boost's architecture.Where to Put the Code
The boost Kagenti module already exists at
plugins/boost-backend-module-kagenti/src/. Key existing files:provider/KeycloakTokenCache.ts— existing cache wrapper using BackstageCacheServiceprovider/KagentiProvider.ts— the provider that will consumeKeycloakTokenManagertypes.ts— module-local type definitionsCreate
KeycloakTokenManager.ts(and.test.ts) in theprovider/directory, following the module's existing patterns.Specifications
openspec/changes/security-safety-governance/specs/access-control/spec.md— Service-account auth scenarios (6 scenarios covering token acquisition, streaming, 401 retry, user identity propagation, config, LlamaStack unaffected)openspec/changes/security-safety-governance/design.md— Decision 4 (service-account auth with token caching and streaming support)openspec/changes/security-safety-governance/tasks.md— Section 7 (all tasks)openspec/changes/platform-operations-deployment/specs/runtime-config/spec.md— Config field scenariosspecifications/boost-context.md— Design principles (read Principle 10 for auth context)