Skip to content

fix(ui): preserve post-login redirect target through oauth2-proxy sign-in - #2533

Merged
EItanya merged 1 commit into
kagent-dev:mainfrom
onematchfox:fix-preserve-log-url-main
Sep 4, 2026
Merged

fix(ui): preserve post-login redirect target through oauth2-proxy sign-in#2533
EItanya merged 1 commit into
kagent-dev:mainfrom
onematchfox:fix-preserve-log-url-main

Conversation

@onematchfox

Copy link
Copy Markdown
Contributor

#2532 - against main

@onematchfox
onematchfox requested review from a team, Charlesthebird and peterj as code owners August 24, 2026 11:13
@github-actions github-actions Bot added the bug Something isn't working label Aug 24, 2026
@onematchfox
onematchfox force-pushed the fix-preserve-log-url-main branch 2 times, most recently from 7b9b2ef to d78bc5c Compare August 27, 2026 11:24

@Charlesthebird Charlesthebird left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The UI on main has been updated to Vite + React Router instead of Next JS. So the UI folder changes will need to be moved around, and this will need to get the search params using useSearchParams.

Comment thread ui/src/lib/loginRedirect.ts Outdated
@onematchfox
onematchfox force-pushed the fix-preserve-log-url-main branch from 024213b to 7a02917 Compare September 4, 2026 13:27
…n-in

When oauth2-proxy intercepts an unauthenticated request it serves its
sign-in page carrying the original destination as `.Redirect` (e.g.
`/oauth2/sign_in?rd=%2Fagents%2Ffoo`). `sign_in.html` template ignored that and unconditionally redirected to `/login`, and `/login`'s
"Sign in with SSO" link was hardcoded to `rd=/` -- so any login, expired-
cookie or not, always landed back on the home page instead of the page
the user was trying to reach.

Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
@onematchfox
onematchfox force-pushed the fix-preserve-log-url-main branch from 52e6907 to 32ddd8e Compare September 4, 2026 13:52
@onematchfox

Copy link
Copy Markdown
Contributor Author

The UI on main has been updated to Vite + React Router instead of Next JS. So the UI folder changes will need to be moved around, and this will need to get the search params using useSearchParams.

Done.

@Charlesthebird Charlesthebird left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This automated review is a first pass. A manual review will follow once the blockers are addressed.


Nothing blocks merge. The core fix works end to end, and both points from the last round are addressed. One Medium: the no-rd fallback branch is unreachable in the real app, and the test covering it asserts a state the router cannot produce.

What I checked

  • sanitizeRedirect returns / for 17 vectors beyond its own tests — ///host, ////host, \\host, \/host, a leading space, tab or newline before //host, https:host, https:/\host, /..//host, //host\@kagent.invalid/, and http://kagent-login-redirect.invalid//evil.
  • /%2F%2Fhost stays percent-encoded through the parser, so it resolves as a path segment rather than a host.
  • ✅ Rendered the named template in a minimal chart: sign_in.html emits {{ or .Redirect "/" | urlquery }} verbatim, and the checksum env var resolves to 41963eed….
  • ✅ oauth2-proxy 10.7's deployment.yaml:257 runs tpl over extraEnv, while podAnnotations at line 51 is plain toYaml — a checksum/config annotation would have shipped the literal {{ include … }}.
  • ✅ The ConfigMap is volume-mounted at values.yaml:751-759, and oauth2-proxy parses /templates once at startup.
  • ✅ Executed the template through Go's html/template: urlquery is not double-applied in the content= attribute or the <script> string, and /a"><script>alert(1)</script> emits as %2Fa%22%3E%3Cscript%3E….
  • ✅ oauth2-proxy's own IsValidRedirect rejects // and /\ independently, so a miss in the UI guard is caught downstream.
  • AuthProvider.tsx:81 calls startReauthentication() while still on the original page, so the automatic path's rd was already correct and stays untouched.

🤖 written by Claude

// all: oauth2-proxy answered with its `sign_in.html`, which forwards to
// `/login?rd=%2Fagents%2Ffoo`.
window.location.assign(
rd === null ? reauthenticationUrl(window.location) : ssoStartUrl(sanitizeRedirect(rd)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Level: 🟠 Medium · Not Blocking

AppHeader.tsx:45 does navigate(paths.login), a pushState onto a standalone route, so startSso reads /login from window.location and this branch always produces rd=%2Flogin.

LoginPage.test.tsx sets window.location.pathname = "/agents/foo" while rendering at /login, which the router cannot produce, so the test passes on a case that never occurs. playwright/tests/auth/auth-modes.spec.ts:101 already reaches that button, so the path is live.

The fix belongs in the unchanged src/components/Structure/AppHeader.tsx:45, so the destination arrives here as rd:

onClick={() =>
  navigate(
    `${paths.login}?rd=${encodeURIComponent(location.pathname + location.search + location.hash)}`,
  )
}

🤖 written by Claude

oauth2-proxy to evaluate, instead of trying to evaluate it itself). It is
forwarded to kagent's branded /login page.
*/}}
{{- define "kagent.oauth2ProxySignInHTML" -}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Level: 🟡 Low · Not Blocking

No helm-unittest suite in helm/kagent/tests/ covers the new ConfigMap, and asserting sign_in.html contains {{ or .Redirect "/" | urlquery }} verbatim is what catches the {{ "{{" }} escaping breaking silently.


🤖 written by Claude

Comment thread helm/kagent/values.yaml
# Cluster-specific OIDC settings - override these per deployment
# These are set as env vars and referenced in args for easy patching
extraEnv:
# Forces a rollout whenever the sign_in.html ConfigMap's content changes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Level: 🟡 Low · Not Blocking

The checksum sits in the list line 788 tells operators to override per deployment, so replacing extraEnv drops the rollout trigger without a word.

Suggested change
# Forces a rollout whenever the sign_in.html ConfigMap's content changes.
# Forces a rollout whenever the sign_in.html ConfigMap's content changes.
# Keep this entry if you override extraEnv, or template changes will not restart the proxy.

🤖 written by Claude

action: "Sign in with SSO",
onClick: startSso,
}
blurb: "Your session has expired. Sign in again to continue.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nit]
Level: 🟡 Low · Not Blocking

This ternary's three object literals were reindented away from the surrounding style (compare AgentTemplateForm.tsx:379 and mcpServerRequest.ts:233), which accounts for 30 of the file's 45 changed lines.


🤖 written by Claude


// There are two ways to arrive here:
// 1. A reader who clicked "Session expired" in the header is still on the
// page they were reading.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nit]
Level: 🟡 Low · Not Blocking

Trailing whitespace, the only such line in ui/src.

Suggested change
// page they were reading.
// page they were reading.

🤖 written by Claude

// crafted `/login?rd=https://evil.example.com` link is a URL anybody can send.
const startSso = () => {
window.location.assign(reauthenticationUrl(window.location));
const rd = searchParams.get("rd");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[nit]
Level: 🟡 Low · Not Blocking

?rd= yields "" rather than null, so it takes the sanitize branch below and lands on / — harmless, though the rd === null split reads as if it covers that case.


🤖 written by Claude

@Charlesthebird Charlesthebird left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for updating this! It looks good to me.

I kicked off the automated review above, and the one medium test note might be worth addressing, but I don't think it's a blocker.

@Charlesthebird
Charlesthebird enabled auto-merge (squash) September 4, 2026 16:42
@Charlesthebird
Charlesthebird enabled auto-merge (squash) September 4, 2026 17:29
@EItanya
EItanya disabled auto-merge September 4, 2026 19:56
@EItanya
EItanya merged commit 7862c96 into kagent-dev:main Sep 4, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants