ZCU-PUB/fix: reload redirect loop after Shibboleth login (absolute reload URL, no SSR redirect) - #1384
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
This PR addresses a post-authentication redirect loop (notably after Shibboleth login) by ensuring reload redirects are emitted as absolute, ui.nameSpace-aware URLs and by preventing SSR from repeatedly issuing the post-login redirect when it can’t clear the redirect cookie.
Changes:
- Build absolute, nameSpace-aware
/reload/<ts>URLs in bothAuthService.navigateToRedirectUrl()andLocaleService.refreshAfterChangeLanguage(), with a guard to avoid re-redirecting while already on a reload page. - Prevent SSR from dispatching the redirect-after-login action (leaving it to the browser after hydration), avoiding redirect loops caused by non-functional server cookie mutation.
- Add tests for SSR vs browser behavior and add server-side hard-redirect defense to avoid emitting relative
Locationheaders.
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 | Updates test config setup to avoid mutating shared environment (but currently introduces a config key mismatch). |
| src/app/core/services/server-hard-redirect.service.ts | Ensures server redirects don’t emit relative Location headers. |
| src/app/core/services/server-hard-redirect.service.spec.ts | Expands redirect tests for relative and external URL handling. |
| src/app/core/locale/locale.service.ts | Makes language-refresh hard redirect use absolute, nameSpace-aware reload URL. |
| src/app/core/locale/locale.service.spec.ts | Adds a spec ensuring refresh redirect uses an absolute, nameSpace-aware reload URL. |
| src/app/core/auth/auth.service.ts | Makes post-login reload redirect absolute and avoids redirecting again when already on reload. |
| src/app/core/auth/auth.service.spec.ts | Updates redirect URL expectations to match absolute, nameSpace-aware reload URLs; adds “no re-redirect on reload page” coverage. |
| src/app/core/auth/auth.effects.ts | Browser-only dispatch of redirect-after-login; SSR retrieves EPerson instead. |
| src/app/core/auth/auth.effects.spec.ts | Adds coverage for browser vs SSR behavior of authenticatedSuccess$ when redirect URL is set. |
Comments suppressed due to low confidence (1)
src/app/static-page/static-page.component.spec.ts:36
APP_CONFIG.uiuses thenameSpaceproperty (capital S) throughout the codebase (seesrc/environments/environment.test.ts:17-23and usages likeapp.module.ts). This spec overridesuiwith{ namespace: ... }, which drops the realuifields (host/port/baseUrl/…) and sets the wrong property name, so any code readingappConfig.ui.nameSpacewill seeundefinedand the test becomes less representative.
// 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.
…ng 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>
…ng 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>
ca9fe67 to
3333bd4
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; 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