fix(ui): preserve post-login redirect target through oauth2-proxy sign-in - #2533
Conversation
7b9b2ef to
d78bc5c
Compare
Charlesthebird
left a comment
There was a problem hiding this comment.
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.
024213b to
7a02917
Compare
…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>
52e6907 to
32ddd8e
Compare
Done. |
Charlesthebird
left a comment
There was a problem hiding this comment.
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
- ✅
sanitizeRedirectreturns/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/, andhttp://kagent-login-redirect.invalid//evil. - ✅
/%2F%2Fhoststays 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.htmlemits{{ or .Redirect "/" | urlquery }}verbatim, and the checksum env var resolves to41963eed…. - ✅ oauth2-proxy 10.7's
deployment.yaml:257runstploverextraEnv, whilepodAnnotationsat line 51 is plaintoYaml— achecksum/configannotation would have shipped the literal{{ include … }}. - ✅ The ConfigMap is volume-mounted at
values.yaml:751-759, and oauth2-proxy parses/templatesonce at startup. - ✅ Executed the template through Go's
html/template:urlqueryis not double-applied in thecontent=attribute or the<script>string, and/a"><script>alert(1)</script>emits as%2Fa%22%3E%3Cscript%3E…. - ✅ oauth2-proxy's own
IsValidRedirectrejects//and/\independently, so a miss in the UI guard is caught downstream. - ✅
AuthProvider.tsx:81callsstartReauthentication()while still on the original page, so the automatic path'srdwas 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)), |
There was a problem hiding this comment.
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" -}} |
There was a problem hiding this comment.
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
| # 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. |
There was a problem hiding this comment.
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.
| # 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.", |
There was a problem hiding this comment.
[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. |
There was a problem hiding this comment.
[nit]
Level: 🟡 Low · Not Blocking
Trailing whitespace, the only such line in ui/src.
| // 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"); |
There was a problem hiding this comment.
[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
left a comment
There was a problem hiding this comment.
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.
#2532 - against main