docs(augment): add OpenSpec proposal for per-user Kagenti token exchange#3330
docs(augment): add OpenSpec proposal for per-user Kagenti token exchange#3330gabemontero wants to merge 2 commits into
Conversation
Add OpenSpec artifacts for the kagenti-user-level-token-exchange change, which implements RFC 8693 OAuth2 Token Exchange for the Kagenti provider enabling per-user authorization instead of shared service-account tokens. Artifacts created: - proposal.md: motivation, capabilities (token-exchange, user-token-routing), impact - design.md: 5 architectural decisions with alternatives, risks, and constraints - specs/token-exchange/spec.md: 7 requirements, 15 scenarios (config, RFC 8693 execution, caching, dedup, streaming, fallback, no-impact guarantee) - specs/user-token-routing/spec.md: 6 requirements, 8 scenarios (header routing, route extraction, interface widening, backward compatibility) - tasks.md: 6 task groups, 19 implementation tasks ordered by dependency Includes the preliminary implementation plan used as source material. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3330 +/- ##
=======================================
Coverage 53.98% 53.98%
=======================================
Files 2401 2401
Lines 87397 87397
Branches 24196 24196
=======================================
Hits 47179 47179
Misses 38670 38670
Partials 1548 1548
*This pull request uses carry forward flags. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
|
||
| ## Context | ||
|
|
||
| The augment plugin's Kagenti provider authenticates to Kagenti using a single shared service-account token via Client Credentials Grant (`KeycloakTokenManager`). All backend requests to Kagenti carry the same identity regardless of which Backstage user initiated the request. The user's identity is passed only as an informational `X-Backstage-User` header. |
There was a problem hiding this comment.
I am puzzled about this direction. In the orchestrator we implemented passing user identity to sonataFlow. Which means we use backstage providers defined and we force users to authenticate to pass valid tokens. Why not to follow similar approach here?
There was a problem hiding this comment.
Good question — we did consider following the Backstage auth provider pattern. The main blocker is that createApiFactory requires all deps to be resolvable at startup — there's no optional dependency concept. Adding oidcAuthApiRef as a hard dependency would crash deployments that don't use OIDC auth, and useApi() throws synchronously during render if the API isn't
registered. This matters for us because the augment plugin needs to work across deployment configurations — not all RHDH deployments use Keycloak/OIDC as their auth provider, and we don't want to force that as a prerequisite just to enable the Kagenti integration.
That's why Decision 1 in the design went with accepting the OIDC token from a configurable request header instead — it keeps token exchange opt-in without coupling the frontend to a specific auth provider.
That said, if the orchestrator team found a way to handle the hard-dependency problem with createApiFactory (or if requiring OIDC auth was acceptable in that context), I'd love to understand the pattern you used. It could be a better approach here too, or at least an alternative alongside the header-based path.
Could you point me at how the orchestrator passes user identity to SonataFlow? Specifically how it gets the user's Keycloak OIDC token in the frontend without the createApiFactory constraint being an issue.
There was a problem hiding this comment.
Here is what claude/opus came up with when analyzing the orchestrator code:
I looked at the orchestrator's approach in useOrchestratorAuth.ts. The pattern there is different from our use case in a couple of ways:
1. The orchestrator collects provider-scoped tokens (GitHub, GitLab, Microsoft) to forward to SonataFlow for external API access. The three built-in providers are always registered by Backstage, so useApi() works safely for those. For custom providers, it uses useApiHolder() with dynamic discovery to avoid the static createApiFactory dependency.
2. For our case, we specifically need the user's Keycloak OIDC token to perform RFC 8693 token exchange against Keycloak. The orchestrator's findCustomProvider does reference internal.auth.oidc from RHDH (line 100-101 in useOrchestratorAuth.ts), but it accesses the API holder's internal map via @ts-ignore — it's reaching into a private API.
The createApiFactory constraint matters for us because the augment plugin needs to work across deployment configurations — not all RHDH deployments use Keycloak/OIDC as their auth provider, and we don't want to force that as a prerequisite just to enable the Kagenti integration. That's why the design went with accepting the OIDC token from a configurable request
header — it keeps token exchange opt-in and decoupled from the frontend auth provider configuration.
That said, the orchestrator's useApiHolder() dynamic discovery pattern is worth exploring as an alternative frontend path in the future. If RHDH stabilizes a public API for accessing the OIDC auth provider, we could add a frontend option alongside the header-based approach.
so yeah, if the useApiHolder bit is part of the underlying orchestrator work you were referring to, perhaps we reassess and go down that path initiall vs. in the future
WDYT @pkliczewski ?
There was a problem hiding this comment.
I know we decided to stay backend. With the orchestrator we use frontend for asking user to login to specific provider if token is not available. In this case a user can login with any provider they want and we will ask them to login with a provider like Keycloak.
If backend only approach I agree with you but if we could ask user to login when connecting to kagenti for the first time it would simplify this change. Maybe we could come up with the flow to force user to login before we connect to kagenti.
…roposal - List supported audience values (Kagenti API client ID, RHDH client ID, or any Keycloak client with exchange permissions) - Use fully qualified `auth.tokenEndpoint` consistently - Add note explaining unsupported_grant_type is a defensive fallback, broaden language from Keycloak to IdP - Defer refresh token support with rationale for re-exchange-on-expiry - Remove duplicate "token exchange disabled" scenario, merge into single "disabled or not configured" scenario - Add inline examples to userTokenHeader in user-token-routing spec - Clarify custom auth mechanism support in Decision 1 - Add log severity levels to Decision 3 fallback cases, document fail-hard as considered-and-rejected alternative - Add Prerequisites section documenting deployment requirements - Explain OIDC token refresh responsibility lies with injection layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com>
|
|
thanks for the thorough review @pkliczewski I've pushed the new commit with changes stemming from your comments I've refrained from resolving any conversations .... certainly at least some of them will require more iteration on our part (and possibly non-trivial adjustments) after you see the updates |
|
@gabemontero thanks, resolved most of the comments |



Hey, I just made a Pull Request!
Add OpenSpec artifacts for the kagenti-user-level-token-exchange change, which implements RFC 8693 OAuth2 Token Exchange for the Kagenti provider enabling per-user authorization instead of shared service-account tokens.
Artifacts created:
Includes the preliminary implementation plan used as source material.
✔️ Checklist