Skip to content

boost-backend — Keycloak service-account auth via OAuth2 Client Credentials (issue 13 of 15) #3309

Description

@gabemontero

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:

  1. 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.)

  2. 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.

  3. 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.

  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions