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 KeycloakAuthClient 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 (KeycloakAuthClient). 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:

All tasks complete. KeycloakAuthClient was deduplicated into boost-node/src/KeycloakAuthClient.ts and is shared by both entity providers (kagenti-entity-provider) and the user-facing provider module (boost-backend-module-kagenti).

Config Fields

The config fields are defined in plugins/boost-backend/src/config/schemas.ts. The KeycloakAuthClient consumer reads 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. KeycloakAuthClient applies its own fallback: const buffer = configValue ?? 60.

  3. Partial config detection: The three core auth fields (tokenEndpoint, clientId, clientSecret) are individually optional in the schema (because Keycloak auth itself is optional). When only a subset of the three fields is present, a warning is logged and auth is disabled. When all three are present, KeycloakAuthClient is constructed. (Implemented in readKagentiAuthConfig in module.ts.)

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

  5. Credential delivery method: Credentials must be sent as client_id and client_secret in the POST form body (application/x-www-form-urlencoded), not via HTTP Basic auth. This matches the Keycloak client configuration (client_secret_post). See access-control/spec.md.

Reference Implementation

The augment workspace has a working KeycloakTokenManager that can be used as a reference for the remaining patterns (401 retry, concurrent deduplication, getTokenForStreaming):

  • workspaces/augment/plugins/augment-backend/src/providers/kagenti/client/KeycloakTokenManager.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, then implement cleanly in boost's architecture.

Where the Code Lives

Completed (PR #3648, PR #3661):

  • plugins/boost-node/src/KeycloakAuthClient.tsKeycloakAuthClient class (deduplicated from kagenti-entity-provider into boost-node for shared use)
  • plugins/boost-node/src/KeycloakAuthClient.test.ts — unit tests
  • plugins/boost-backend-module-kagenti/src/provider/KagentiApiClient.ts — 401 retry, bearer token injection, X-Backstage-User header
  • plugins/boost-backend-module-kagenti/src/provider/KagentiProvider.tsChatOptions threading for user identity propagation
  • plugins/boost-backend-module-kagenti/src/module.ts — config reading, partial config detection, auth client construction

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

    Fields

    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