ZCU-DATA/fix: reload redirect loop after Shibboleth login (absolute reload URL, no SSR redirect) - #1385
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes a post-authentication redirect loop (notably after Shibboleth login and during SSR) by ensuring reload redirects are absolute, namespace-aware, and only performed client-side when needed.
Changes:
- Build absolute,
ui.nameSpace-aware/reload/<ts>URLs in auth redirect and language refresh flows, with loop-avoidance when already on a reload route. - Prevent SSR from repeatedly dispatching post-login redirects by gating redirect dispatch to browser-only execution.
- Add defense-in-depth on the server to avoid emitting relative
Locationheaders; update/add corresponding unit tests.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/static-page/static-page.component.spec.ts | Avoids mutating the shared environment in tests (but introduces a config key mismatch issue noted in comments). |
| src/app/core/services/server-hard-redirect.service.ts | Ensures server redirects emit absolute URLs (leading / for relative inputs). |
| src/app/core/services/server-hard-redirect.service.spec.ts | Updates redirect expectations and adds coverage for relative vs external URLs. |
| src/app/core/locale/locale.service.ts | Makes reload redirect absolute and namespace-aware after language changes. |
| src/app/core/locale/locale.service.spec.ts | Adds test ensuring the reload URL is absolute and namespace-aware. |
| src/app/core/auth/auth.service.ts | Makes post-login reload redirect absolute/namespace-aware and avoids redirecting when already on reload. |
| src/app/core/auth/auth.service.spec.ts | Updates redirect URL assertions and adds coverage for “already on reload” case. |
| src/app/core/auth/auth.effects.ts | Dispatches redirect-after-login only in the browser; SSR retrieves EPerson instead. |
| src/app/core/auth/auth.effects.spec.ts | Adds coverage for browser vs SSR behavior when a redirect URL exists. |
Comments suppressed due to low confidence (1)
src/app/static-page/static-page.component.spec.ts:36
APP_CONFIGexpectsui.nameSpace(used byStaticPageComponent.processLinks), but this spec setsui.namespace. That leavesappConfig.ui.nameSpaceundefined and can break link-handling tests (or future changes) in this suite.
// Do not mutate the shared `environment` object - replacing `environment.ui` would
// break any later spec that reads e.g. environment.ui.nameSpace
appConfig = Object.assign({}, environment, {
ui: {
namespace: 'testNamespace'
}
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ing SSR Replicates the redirect loop after a Shibboleth login: the relative 'reload/<timestamp>' URL is resolved against the current URL (e.g. /bitstreams/<uuid>/download), producing growing /bitstreams/<uuid>/reload/reload/... URLs, because during SSR the redirect repeats on every request (the dsRedirectUrl cookie cannot be cleared server-side - ServerCookieService.remove is a no-op). Also stops static-page.component.spec from mutating the shared environment object (it replaced environment.ui, breaking later specs reading environment.ui.nameSpace). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing SSR Fixes the redirect loop after a Shibboleth login: - navigateToRedirectUrl() and refreshAfterChangeLanguage() build an absolute, ui.nameSpace-aware /reload/<ts> URL. A relative 'reload/<ts>' URL in the Location header is resolved against the request URL (e.g. /bitstreams/<uuid>/download after an external login), producing nested /bitstreams/<uuid>/reload/... URLs which never match the top-level 'reload/:rnd' route. navigateToRedirectUrl() also skips the redirect when the current path already is the reload page. - authenticatedSuccess$ dispatches RedirectAfterLoginSuccess only in the browser. The server cannot clear the dsRedirectUrl cookie (ServerCookieService.set/remove are no-ops), so a hard redirect during SSR repeats on every request - one 'reload/' segment per 302 hop - until the browser aborts at its redirect limit. - ServerHardRedirectService.redirect() never emits a relative Location header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a655e69 to
7d91529
Compare
Problem
After a Shibboleth login the browser can end up in a redirect loop with a growing URL, e.g.
/bitstreams/<uuid>/reload/reload/…(18x)…/<timestamp>?redirect=%2Fbitstreams%2F<uuid>%2Fdownload, and the user cannot navigate back (reported by a customer).Two defects combine:
AuthService.navigateToRedirectUrl()andLocaleService.refreshAfterChangeLanguage()build a relativereload/<timestamp>URL. In the browser this stays hidden, becauselocation.replace()resolves relative URLs against<base href>. But in an HTTP Location header there is no base - the browser resolves it against the request URL, so from/bitstreams/<uuid>/downloadit becomes/bitstreams/<uuid>/reload/<ts>, which never matches the top-levelreload/:rndroute. Inherited from upstream (present ondspace-7_xandmain).dsRedirectUrlcookie (ServerCookieService.set/removeare no-ops), so whenever a validdsAuthInfocookie coexists with adsRedirectUrlcookie on a full page load, every SSR pass emits another 302 with the relative Location header - one extrareload/per hop, until the browser aborts at its redirect limit (the reported URL has 18).Fix
navigateToRedirectUrl()builds an absolute,ui.nameSpace-aware/reload/<ts>URL and skips the redirect when the current path already is the reload page.refreshAfterChangeLanguage().authenticatedSuccess$dispatches the redirect action only in the browser; on the server it retrieves the EPerson instead. The browser re-runs the auth check after hydration, performs the redirect and can actually clear the cookie.ServerHardRedirectService.redirect()never emits a relative Location header.Reproduced live (dev instance, build without the fix)
The loop needs the state a not-finished login attempt leaves behind: a
dsRedirectUrlcookie (lives 1 hour) alongside a validdsAuthInfocookie, plus one full page load of a nested URL. Verified on a dev instance - one page load produced 19 server 302s andERR_TOO_MANY_REDIRECTS, with exactly the reported URL shape:Steps:
dsAuthInfocookie.document.cookie = 'dsRedirectUrl=' + encodeURIComponent('/bitstreams/<uuid>/download') + '; path=/; secure'/bitstreams/<uuid>/download.reload/segment per hop until the browser stops withERR_TOO_MANY_REDIRECTS. The Back button re-enters the chain as long as the cookie lives.The clean login round trip itself does not loop (the browser clears the cookie in time) - which is why the issue appears intermittently in production, whenever the stale cookie survives into a logged-in full page load.
TDD
Commit 1 adds/updates the specs. Run locally without the fix - 8 failures:
Commit 2 fixes the code - all pass:
(The images are rendered from the real local
ng testlogs of the zcu-pub twin branch - the changes are identical on both branches; the full local unit-test suite with the fix: 4843/4843 SUCCESS. Note: the first commit intentionally fails CI when checked out alone - TDD ordering. Evidence images live on the deletable branchassets/zcu-shibboleth-pr-evidence.)Notes