OIDC proxy auth#1293
Conversation
f8b5fb5 to
b608370
Compare
Introduces optional OIDC authentication via oauth2-proxy subchart. When controller.auth.mode is set to "proxy", the controller trusts JWT tokens from the Authorization header (set by oauth2-proxy) instead of the default unsecure X-User-Id header. Includes proxy authenticator, /api/me endpoint, auth header forwarding in UI server actions, AuthContext/login page in the frontend, network policies, and Helm configuration for oauth2-proxy integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Collin Walker <cwalker@ancestry.com> Signed-off-by: Collin Walker <lets-call-n-walk@users.noreply.github.com>
b608370 to
fcb1244
Compare
There was a problem hiding this comment.
Pull request overview
Adds an optional “OIDC proxy auth” mode to kagent, intended to run behind oauth2-proxy and derive user identity from a JWT in the Authorization header, while keeping the existing unauthenticated (“unsecure”) behavior as the default.
Changes:
- Backend: introduce
ProxyAuthenticator, extendPrincipalwith identity fields, and add/api/mefor current-user introspection. - Frontend: add auth header forwarding utilities, an
AuthContext+UserMenu, and a branded/loginpage. - Helm: add optional oauth2-proxy subchart + templates, auth-related values/env wiring, and NetworkPolicies to restrict direct UI/controller access when auth is enabled.
Reviewed changes
Copilot reviewed 36 out of 38 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/types/index.ts | Adjust UI request typing for session creation (removes user_id from request shape). |
| ui/src/lib/jwt.ts | JWT decode + claim extraction utilities for UI/server actions. |
| ui/src/lib/auth.ts | Centralized Authorization header extraction for server actions/route handlers. |
| ui/src/contexts/AuthContext.tsx | Client-side provider for “current user” state via a server action. |
| ui/src/components/chat/ChatInterface.tsx | Stops injecting user_id when creating sessions. |
| ui/src/components/UserMenu.tsx | Adds user dropdown with groups display + sign out. |
| ui/src/components/Header.tsx | Wires UserMenu into desktop/mobile header layouts. |
| ui/src/app/tools/page.tsx | Fixes import path to use app actions alias. |
| ui/src/app/servers/page.tsx | Fixes import path to use app actions alias. |
| ui/src/app/login/page.tsx | Adds branded login page with SSO redirect link. |
| ui/src/app/layout.tsx | Wraps app in AuthProvider and adjusts root layout hydration settings. |
| ui/src/app/actions/utils.ts | Stops appending user_id to backend requests; forwards Authorization instead. |
| ui/src/app/actions/feedback.ts | Removes user_id injection from feedback submission. |
| ui/src/app/actions/auth.ts | Adds server action to derive current user from incoming Authorization JWT. |
| ui/src/app/a2a/[namespace]/[agentName]/route.ts | Forwards auth header through the A2A proxy route. |
| ui/package.json | Adds jose dependency for JWT decoding. |
| helm/kagent/values.yaml | Adds controller/ui auth values, oauth2-proxy values, and networkPolicy values. |
| helm/kagent/templates/ui-deployment.yaml | Injects SSO_REDIRECT_PATH env var into UI deployment. |
| helm/kagent/templates/oauth2-proxy-templates.yaml | Adds custom oauth2-proxy template ConfigMap to redirect to /login. |
| helm/kagent/templates/networkpolicy.yaml | Adds NetworkPolicies to restrict ingress paths when proxy auth is enabled. |
| helm/kagent/templates/controller-deployment.yaml | Injects AUTH_MODE and JWT-claim env vars into controller when in proxy mode. |
| helm/kagent/templates/_helpers.tpl | Adds helper to conditionally enable NetworkPolicies when auth is enforced. |
| helm/kagent/templates/NOTES.txt | Documents NetworkPolicy behavior when enabled. |
| helm/kagent/Chart-template.yaml | Adds oauth2-proxy as an optional chart dependency. |
| go/test/e2e/auth_api_test.go | Adds E2E coverage for /api/me across auth modes. |
| go/pkg/auth/auth_test.go | Adds basic test for new Principal user/group fields. |
| go/pkg/auth/auth.go | Extends auth model with Email, Name, and Groups. |
| go/internal/httpserver/server.go | Adds /api/me route and nil-safety for DB manager shutdown. |
| go/internal/httpserver/handlers/handlers.go | Registers CurrentUserHandler. |
| go/internal/httpserver/handlers/current_user_test.go | Unit tests for /api/me handler behavior. |
| go/internal/httpserver/handlers/current_user.go | Implements /api/me handler using auth session principal. |
| go/internal/httpserver/auth/proxy_authn_test.go | Unit tests for JWT claim extraction + fallback behaviors. |
| go/internal/httpserver/auth/proxy_authn.go | Implements proxy-mode authenticator (JWT parsing + fallback). |
| go/cmd/controller/main.go | Selects authenticator based on AUTH_MODE at startup. |
| go/cmd/controller/auth_mode_test.go | Tests authenticator selection logic. |
| docs/OIDC_PROXY_AUTH_ARCHITECTURE.md | Adds architecture documentation for the proxy auth approach. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const SSO_REDIRECT_PATH = process.env.SSO_REDIRECT_PATH || "/oauth2/start"; | ||
|
|
||
| // Kagent logo SVG - extracted from official logo | ||
| function KagentLogo({ size = 32 }: { size?: number }) { |
There was a problem hiding this comment.
there's a logo here -
-- can that be re-used instead of creating a new one?|
|
||
| {/* CSS to hide header/footer and override main layout for fullscreen login */} | ||
| <style>{` | ||
| body:has(.login-page) > div header, |
There was a problem hiding this comment.
the project is using tailwind, can you convert this to tailwind classes instead?
| <span>kagent</span> | ||
| </h1> | ||
| <p className="text-lg md:text-2xl text-gray-300 max-w-[600px] font-normal mb-10 leading-relaxed"> | ||
| Orchestrating Your AI Future with Kubernetes and Beyond |
There was a problem hiding this comment.
since we have "Bringing Agentic AI to cloud native" on the website -- perhaps we do the same here?
| } | ||
|
|
||
| export function extractUserFromClaims(claims: JWTPayload) { | ||
| return { |
There was a problem hiding this comment.
can we introduce a type for the { user, email, name, groups }?
| return ( | ||
| <> | ||
| {/* Preload background image for faster rendering */} | ||
| <link rel="preload" href="/login-bg.webp" as="image" type="image/webp" fetchPriority="high" /> |
There was a problem hiding this comment.
if the jpg version of the file is not used, we should remove it from the /public folder
9129572 to
989f6ed
Compare
Signed-off-by: Collin Walker <lets-call-n-walk@users.noreply.github.com>
989f6ed to
c719dc1
Compare
EItanya
left a comment
There was a problem hiding this comment.
These changes are looking great! The 2 themes I wanna focus on first are the inclusion of NetworkPolicy and the way the claims are being parsed. Looking forward to more discussion and iteration!
Signed-off-by: Collin Walker <lets-call-n-walk@users.noreply.github.com>
EItanya
left a comment
There was a problem hiding this comment.
Overall the changes here make a lot of sense to me. Unfortunately the PR seems quite out of date so I'm having trouble gleaning what exactly is new and what isn't. Any chance you can update so I can look again?
| // Agents authenticate via user_id query param or X-User-Id header | ||
| userID := query.Get("user_id") | ||
| if userID == "" { | ||
| userID = reqHeaders.Get("X-User-Id") | ||
| } | ||
| if userID == "" { | ||
| return nil, ErrUnauthenticated | ||
| } | ||
|
|
||
| return &SimpleSession{ | ||
| P: auth.Principal{ | ||
| User: auth.User{ | ||
| ID: userID, | ||
| }, | ||
| Agent: auth.Agent{ | ||
| ID: agentID, | ||
| }, | ||
| }, | ||
| authHeader: authHeader, | ||
| }, nil |
There was a problem hiding this comment.
We should probably create a follow-up for this to make sure it's more secure
| // Parse JWT without validation (oauth2-proxy or k8s service account already validated) | ||
| rawClaims, err := parseJWTPayload(tokenString) |
There was a problem hiding this comment.
Can we call this mode trusted-proxy instead of just proxy because it's explicitly not re-validating
Signed-off-by: Collin Walker <10523817+lets-call-n-walk@users.noreply.github.com>
Clarifies that this mode explicitly trusts the upstream proxy's JWT validation and does not re-validate tokens. Signed-off-by: Collin Walker <lets-call-n-walk@users.noreply.github.com>
…/kagent into oidc-proxy-auth
|
@EItanya renamed proxy to trusted proxy. Most of the other comments were on code that wasn't mine, but the diff had gotten messed up from being behind - I merged and the diff is correct now. |
| async function submitFeedback(feedbackData: FeedbackData): Promise<any> { | ||
| const userID = await getCurrentUserId(); | ||
| const body = { | ||
| const body = { |
There was a problem hiding this comment.
Removing user_id from this payload looks like a regression unless the backend now injects it from the auth session. HandleCreateFeedback still persists the request body verbatim, so database.Feedback.UserID is stored as empty here and ListFeedback(userID) will never return these rows.
| value: "" | ||
| # Default assumes release name "kagent". Override if using different release name. | ||
| - name: UPSTREAM_URL | ||
| value: "http://kagent-ui:8080" |
There was a problem hiding this comment.
Hard-coding http://kagent-ui:8080 only works for the default release name. The chart templates the actual service name as {{ include "kagent.fullname" . }}-ui, so fullnameOverride or a non-default release name will break oauth2-proxy unless the user manually overrides UPSTREAM_URL.
| @@ -165,10 +169,13 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) { | |||
|
|
|||
| commandLine.Var(&cfg.Streaming.MaxBufSize, "streaming-max-buf-size", "The maximum size of the streaming buffer.") | |||
| commandLine.Var(&cfg.Streaming.InitialBufSize, "streaming-initial-buf-size", "The initial size of the streaming buffer.") | |||
| commandLine.DurationVar(&cfg.Streaming.Timeout, "streaming-timeout", 600*time.Second, "The timeout for the streaming connection.") | |||
| commandLine.DurationVar(&cfg.Streaming.Timeout, "streaming-timeout", 60*time.Second, "The timeout for the streaming connection.") | |||
There was a problem hiding this comment.
This changes the default streaming timeout from 10 minutes to 1 minute. A2ARegistrar wires this value straight into a2aclient.WithTimeout, so long-running streams and tool calls will now terminate after 60s unless the operator overrides the flag. Was that intentional for this auth PR?
| }, nil | ||
| } | ||
|
|
||
| func (a *ProxyAuthenticator) UpstreamAuth(r *http.Request, session auth.Session, upstreamPrincipal auth.Principal) error { |
There was a problem hiding this comment.
This drops X-User-Id on controller-to-agent A2A calls. The downstream A2A runtimes still derive the caller from that header, so in trusted-proxy mode the real user becomes A2A_USER_<context_id> after the first hop. We likely need to forward the resolved user id here in addition to the bearer token.
In addition to the reason above, passing JWT directly also allows for supporting user identity propagation to downstream services. Some enterprise Cyber practices don't like OBO and prefer propagation. I think it's great to be able to support both with kagent directly being able to propagate identity, and then having agent gateway integration for OBO and more fine-grained control. |
|
This pull request has been marked as stale because of no activity in the last 15 days. It will be closed in the next 5 days unless it is tagged "no stalebot" or other activity occurs. |
Not stale! Would love to see this completed |
|
This is a must have for the project! Please let me know what is needed to get this merged. Happy to assist @lets-call-n-walk @EItanya |
Resolves conflicts from Go directory restructuring (go/ → go/core/), Helm controller deployment env vars, values.yaml, and UI package versions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ream URL - Reject unknown auth modes at startup instead of silently falling back to unsecure; fix help text to document "trusted-proxy" (not "proxy") - Populate feedback UserID from auth context in HandleCreateFeedback so records are queryable by ListFeedback - Add CurrentUser handler to Handlers struct for /api/me endpoint - Strengthen oauth2-proxy UPSTREAM_URL comment re: release name dependency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Collin Walker <cw404@hotmail.com>
3b67c92 to
839a797
Compare
|
@EItanya @jsonmp-k8 updated per review and fixed merge conflicts |
- Add jose to package-lock.json (was missing, breaking all UI/Docker builds) - Fix gofmt formatting in server.go and app.go (spaces -> tabs) - Replace untyped CurrentUser alias with interface defining well-known OIDC claim fields (sub, name, email, groups, preferred_username) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
|
Hey there, sorry for the delay, I updated the PR so it should now pass in CI. I will do a final review tomorrow |
The PR changed the lazy initializer pattern to useState(null) + useEffect, which triggers the no-setState-in-effect ESLint rule. Restore the original lazy initializer while keeping the pathname-based login page guard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
The Header component now renders UserMenu which requires AuthContext. Add AuthProvider decorator to Header stories to fix storybook tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
Without this, downstream A2A runtimes fall back to deriving the caller identity from the context ID (`A2A_USER_<contextID>`) instead of the real authenticated user, because `UserIDCallInterceptor` reads `X-User-Id` rather than the forwarded `Bearer` token. Mirrors the behaviour of [`UnsecureAuthenticator.UpstreamAuth`](https://github.com/kagent-dev/kagent/blob/b50661f858787f6c4de2a73479f492ba82af6ca4/go/core/internal/httpserver/auth/authn.go#L48) which already sets this header. Addresses kagent-dev#1293 (comment)
Without this, downstream A2A runtimes fall back to deriving the caller identity from the context ID (`A2A_USER_<contextID>`) instead of the real authenticated user, because `UserIDCallInterceptor` reads `X-User-Id` rather than the forwarded `Bearer` token. Mirrors the behaviour of [`UnsecureAuthenticator.UpstreamAuth`](https://github.com/kagent-dev/kagent/blob/b50661f858787f6c4de2a73479f492ba82af6ca4/go/core/internal/httpserver/auth/authn.go#L48) which already sets this header. Addresses kagent-dev#1293 (comment) Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Without this, downstream A2A runtimes fall back to deriving the caller identity from the context ID (`A2A_USER_<contextID>`) instead of the real authenticated user, because `UserIDCallInterceptor` reads `X-User-Id` rather than the forwarded `Bearer` token. Mirrors the behaviour of [`UnsecureAuthenticator.UpstreamAuth`](https://github.com/kagent-dev/kagent/blob/b50661f858787f6c4de2a73479f492ba82af6ca4/go/core/internal/httpserver/auth/authn.go#L48) which already sets this header. Addresses kagent-dev#1293 (comment) Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Without this, downstream A2A runtimes fall back to deriving the caller identity from the context ID (`A2A_USER_<contextID>`) instead of the real authenticated user, because `UserIDCallInterceptor` reads `X-User-Id` rather than the forwarded `Bearer` token. Mirrors the behaviour of [`UnsecureAuthenticator.UpstreamAuth`](https://github.com/kagent-dev/kagent/blob/b50661f858787f6c4de2a73479f492ba82af6ca4/go/core/internal/httpserver/auth/authn.go#L48) which already sets this header. Addresses kagent-dev#1293 (comment) Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Without this, downstream A2A runtimes fall back to deriving the caller identity from the context ID (`A2A_USER_<contextID>`) instead of the real authenticated user, because `UserIDCallInterceptor` reads `X-User-Id` rather than the forwarded `Bearer` token. Mirrors the behaviour of [`UnsecureAuthenticator.UpstreamAuth`](https://github.com/kagent-dev/kagent/blob/b50661f858787f6c4de2a73479f492ba82af6ca4/go/core/internal/httpserver/auth/authn.go#L48) which already sets this header. Addresses kagent-dev#1293 (comment) Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Without this, downstream A2A runtimes fall back to deriving the caller identity from the context ID (`A2A_USER_<contextID>`) instead of the real authenticated user, because `UserIDCallInterceptor` reads `X-User-Id` rather than the forwarded `Bearer` token. Mirrors the behaviour of [`UnsecureAuthenticator.UpstreamAuth`](https://github.com/kagent-dev/kagent/blob/b50661f858787f6c4de2a73479f492ba82af6ca4/go/core/internal/httpserver/auth/authn.go#L48) which already sets this header. Addresses kagent-dev#1293 (comment) Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
…1775) Ensures that caller identity correctly propagates from controller->agent->controller. Addresses #1293 (comment) and potentially also #1771 --------- Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com> Co-authored-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io>
Summary
Adds optional OIDC proxy-based authentication to kagent, enabling integration with enterprise identity providers (Cognito, Okta, Dex, etc.) via an oauth2-proxy subchart.
When
controller.auth.modeis set to"proxy", the controller trusts JWT tokens from theAuthorizationheader injected by oauth2-proxy, extracting user identity from configurable JWT claims. When set to"unsecure"(the default), behavior is unchanged — no auth is required.Note: This does not implement Access Control. It is purely the authentication mechanism. Access control is being discussed in this issue: #1270
What's included
Backend (Go)
ProxyAuthenticatorthat extracts user identity (email, name, groups) from JWT claims with configurable claim mappingAUTH_MODEenv var to switch betweenunsecureandproxyauthentication at startup/api/meendpoint returning the current user's identity from the auth contextFrontend (Next.js)
AuthContextprovider that decodes the JWT from theAuthorizationheader and exposes user state/loginpage with branded SSO redirectUserMenucomponent showing current user info with sign-outjosedependency for client-side JWT decodingHelm
controller.authvalues for auth mode and JWT claim mappingui.auth.ssoRedirectPathfor configurable SSO redirectNetworkPolicytemplates that lock down UI/controller access when auth is enabledNOTES.txtadditions documenting auth configurationHow it works
NetworkPolicies ensure UI and controller only accept traffic from oauth2-proxy when auth mode is
proxy.Enabling
See
docs/OIDC_PROXY_AUTH_ARCHITECTURE.mdfor full architecture details.