Skip to content

docs(augment): add OpenSpec proposal for per-user Kagenti token exchange#3330

Draft
gabemontero wants to merge 2 commits into
redhat-developer:mainfrom
gabemontero:kagenti-user-level-token-exchange-proposal
Draft

docs(augment): add OpenSpec proposal for per-user Kagenti token exchange#3330
gabemontero wants to merge 2 commits into
redhat-developer:mainfrom
gabemontero:kagenti-user-level-token-exchange-proposal

Conversation

@gabemontero

Copy link
Copy Markdown
Contributor

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:

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

✔️ Checklist

  • [n/a] A changeset describing the change and affected packages. (more info)
  • [/] Added or Updated documentation
  • [n/a] Tests for new functionality and regression tests for bug fixes
  • [n/a] Screenshots attached (for UI changes)

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

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.98%. Comparing base (5bc0a03) to head (c607ed5).
⚠️ Report is 11 commits behind head on main.
✅ All tests successful. No failed tests found.

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           
Flag Coverage Δ *Carryforward flag
adoption-insights 83.58% <ø> (ø) Carriedforward from 7f3bde9
ai-integrations 70.03% <ø> (ø) Carriedforward from 7f3bde9
app-defaults 69.60% <ø> (ø) Carriedforward from 7f3bde9
augment 46.39% <ø> (ø)
bulk-import 72.86% <ø> (ø) Carriedforward from 7f3bde9
cost-management 17.48% <ø> (ø) Carriedforward from 7f3bde9
dcm 59.64% <ø> (ø) Carriedforward from 7f3bde9
extensions 62.24% <ø> (ø) Carriedforward from 7f3bde9
global-floating-action-button 74.30% <ø> (ø) Carriedforward from 7f3bde9
global-header 61.63% <ø> (ø) Carriedforward from 7f3bde9
homepage 51.52% <ø> (ø) Carriedforward from 7f3bde9
install-dynamic-plugins 56.23% <ø> (ø) Carriedforward from 7f3bde9
konflux 91.01% <ø> (ø) Carriedforward from 7f3bde9
lightspeed 68.47% <ø> (ø) Carriedforward from 7f3bde9
mcp-integrations 85.46% <ø> (ø) Carriedforward from 7f3bde9
orchestrator 37.33% <ø> (ø) Carriedforward from 7f3bde9
quickstart 62.09% <ø> (ø) Carriedforward from 7f3bde9
sandbox 79.42% <ø> (ø) Carriedforward from 7f3bde9
scorecard 83.84% <ø> (ø) Carriedforward from 7f3bde9
theme 64.54% <ø> (ø) Carriedforward from 7f3bde9
translations 8.49% <ø> (ø) Carriedforward from 7f3bde9
x2a 78.79% <ø> (ø) Carriedforward from 7f3bde9

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5bc0a03...c607ed5. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread workspaces/augment/openspec/changes/kagenti-user-level-token-exchange/design.md Outdated
…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>
@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

@gabemontero

Copy link
Copy Markdown
Contributor Author

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

@pkliczewski

Copy link
Copy Markdown

@gabemontero thanks, resolved most of the comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants