feat: add client_credentials fallback for outbound requests without Authorization header#171
Conversation
Migrate all GitHub Issue Agent demo files from the Keycloak "demo" realm to the shared "kagenti" realm. This aligns the demo with the platform-wide realm migration (kagenti#764) so that UI-issued tokens and demo tokens share the same issuer, enabling UI chat with AuthBridge-protected agents. Updated files: - k8s/configmaps.yaml: KEYCLOAK_REALM, TOKEN_URL, ISSUER - k8s/github-tool-deployment.yaml: ISSUER, JWKS_URL - k8s/git-issue-agent-deployment.yaml: JWKS_URI - setup_keycloak.py: default KEYCLOAK_REALM - demo-manual.md: all realm references in docs and commands Part of: rossoctl/rossoctl#767 Signed-off-by: Mariusz Sabath <mrsabath@gmail.com> Made-with: Cursor
Update all Keycloak realm references in demo-ui.md from "demo" to "kagenti" to align with the platform-wide realm migration (kagenti#764). With the UI and demos now on the same realm, the UI chat with AuthBridge-protected agents works end-to-end. The Step 8 known-limitation workaround (kagenti-extensions#147) is replaced with instructions for direct UI chat. Part of: rossoctl/rossoctl#767 Signed-off-by: Mariusz Sabath <mrsabath@gmail.com> Made-with: Cursor
The CLI test steps were obtaining the Keycloak admin token from the master realm. Now that the admin user exists in the kagenti realm with realm-admin permissions, use the kagenti realm consistently throughout the demo to avoid cross-realm dependencies. The wrong-issuer test and troubleshooting sections intentionally retain master realm references. Signed-off-by: Mariusz Sabath <mrsabath@gmail.com> Made-with: Cursor
The Kagenti installer (rossoctl/rossoctl#851) now creates correct default ConfigMaps for all AuthBridge components: - authbridge-config with templatized kagenti realm - envoy-config with 300s timeouts and Lua filter for inbound direction Remove environments, spiffe-helper-config, and envoy-config from the demo's configmaps.yaml — only authbridge-config remains, overriding TARGET_AUDIENCE (github-tool), TARGET_SCOPES, and EXPECTED_AUDIENCE with demo-specific values. Update demo-ui.md Step 2 and demo-manual.md Step 3 to explain that only authbridge-config is overridden. Update troubleshooting sections to reference the installer's envoy-config defaults. Signed-off-by: Mariusz Sabath <mrsabath@gmail.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Keycloak only auto-assigns realm default scopes to newly created clients. The UI kagenti client was created during install before the agent audience scope existed, so it does not inherit it. Without this scope, the UI token lacks the agent SPIFFE ID in the audience claim, causing AuthBridge to reject UI chat requests with invalid audience. Explicitly add the agent-ns-sa-aud scope to the kagenti UI client after creating it as a realm default. This is a temporary workaround until the client-registration sidecar handles this automatically (rossoctl#169). Signed-off-by: Mariusz Sabath <mrsabath@gmail.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
…uthorization header When an outbound request has no Authorization header, the ext_proc now falls back to a client_credentials grant using the agent's shared credentials (/shared/client-id.txt + /shared/client-secret.txt). This handles agent frameworks that do not propagate the inbound Bearer token to outbound tool calls. The token exchange path (RFC 8693) is still preferred when an Authorization header IS present, as it preserves the end user's identity (sub claim). The client_credentials fallback uses the agent's own identity instead. Closes rossoctl#170 Signed-off-by: Mariusz Sabath <mrsabath@gmail.com> Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
|
@mrsabath need some clarifications on this PR. Our original assumption was that the (A2A) agent would take the inbound token (i.e., from the UI or CLI) that has user info, and passes that token when it makes tools calls or calls other agents. However, it seems that we cannot make that assumption and we would need to rely on static credentials embedded in the agent to make the token exchange. This begs the question if the agent is shared between multiple users (e.g., Alice and Bob), how would authbridge do the correct token exchange on behalf of Alice or Bob as the credential it uses for the token exchange is tied to the agent itself, and nothing to do with Alice or Bob. |
|
Great question, Hai. You're right that this is a key trade-off. Here's my thinking: The core problem: Current agent frameworks (A2A SDK, LangChain, etc.) don't propagate the inbound Bearer token to outbound HTTP calls. The agent receives Alice's token on inbound, processes the prompt with the LLM, and makes a fresh HTTP call to the tool — without any Authorization header. Without this PR, the tool gets a 401. This PR is a pragmatic fallback, not a replacement for token exchange. The code still prefers token exchange when an Authorization header IS present (lines 511-535). Client_credentials only kicks in when there's no token to exchange. If/when agent frameworks start propagating the caller's token, the token exchange path activates automatically — no code changes needed. What we lose: With client_credentials, the tool sees the agent's SPIFFE ID, not the end user. Per-user authorization at the tool level isn't possible from the token alone. What still works: Inbound validation still identifies the user (Alice vs Bob). Scope-based access (public vs privileged PAT) is configured at the agent level and still works. The tool just can't distinguish which user triggered a particular call. The industry is actively working on this gap. Two IETF drafts from late 2025 define "Transaction Tokens for Agents" — tokens carrying both an Proposed path forward:
I'll file an issue to track the token propagation investigation. |
|
You're right, I need to correct that claim. With client_credentials, the agent requests With token exchange, Keycloak evaluates the subject token (Alice's or Bob's) and can restrict the exchanged token's scopes based on what the subject actually has. That's the mechanism that makes Alice → public access, Bob → privileged access work. So to be precise about what the client_credentials fallback provides:
The client_credentials path is a "get it working" fallback that trades per-user access control for functionality. The right long-term fix is agent-side token propagation (#174), which would activate the existing token exchange path automatically. |
|
Good idea — since the ext_proc is a single Go process that handles both inbound and outbound, it can maintain in-memory state. The question is: what key do we use to correlate an outbound request back to the inbound user? The architectural challenge: Inbound and outbound are on separate Envoy listeners with separate gRPC streams to the ext_proc. There's no shared Envoy context (no connection ID, no session ID) that links them. Why correlation headers don't help: The "last token" approach — works for simple agents: var lastInboundToken atomic.Value // set on inbound, read on outboundBut this is unsafe for concurrent multi-user agents — Alice's outbound could pick up Bob's cached token, causing privilege escalation. We'd need to document this clearly and gate it behind a feature flag (e.g., Summary of approaches:
I think the "last-token cache" is worth exploring as an opt-in middle ground for single-user/non-concurrent agents. It solves the per-user problem for simple deployments without requiring agent framework changes, at the cost of concurrency safety. Want me to prototype it behind a feature flag? |
huang195
left a comment
There was a problem hiding this comment.
Review Summary
This PR adds a valuable OAuth2 client_credentials fallback for agent frameworks that don't propagate inbound Bearer tokens, plus significant cleanup migrating from the demo to kagenti realm and slimming ConfigMaps. The main concern is that the Go fallback path silently continues without auth when token acquisition fails — this should return an error response to prevent unauthenticated requests reaching downstream services. The ConfigMap reduction and Keycloak setup improvements are well done.
Areas reviewed: Go, Python, YAML/K8s, Docs, Commit conventions, Security
Commits: 6 commits, all signed-off (DCO passing)
CI status: passing
Great cleanup on configmaps.yaml — reducing from 237 to 55 lines by leveraging installer defaults eliminates config drift. The setup_keycloak.py workaround for scope inheritance is well-documented with a clear pointer to #169 for the permanent fix.
When clientCredentialsGrant() or exchangeToken() fails, the ext_proc was falling through and sending the outbound request without any Authorization header. This caused confusing downstream 401 errors far from the root cause. Now return 503 Service Unavailable with a clear error message so callers get an immediate signal that token acquisition failed. Addresses review feedback from @huang195 on PR rossoctl#171. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Mariusz Sabath <mrsabath@gmail.com>
Summary
clientCredentialsGrant()function to go-processor for OAuth 2.0 client_credentials grant/shared/client-id.txt+/shared/client-secret.txt)[Client Credentials]to distinguish from[Token Exchange]Context
Many agent frameworks (including the A2A SDK) do not propagate the inbound Bearer token to outbound tool calls. The agent code assumes AuthBridge handles outbound auth transparently, but the ext_proc previously required an existing token to exchange. Without this fallback, the tool receives no Authorization header and returns 401.
Files changed
AuthBridge/AuthProxy/go-processor/main.goclientCredentialsGrant(), add fallback inhandleOutbound()Closes #170
Test plan
[Client Credentials]token injection