Follow-up to #32365 / PR #36590 (which broke the infinite loop and added the default 403 "Access Denied" page + deterministic loop-guard). Two related improvements were deliberately deferred:
1. Return the user to the page they requested (default RelayState)
Today, when a front-end SAML user authenticates, the original front-end URL is lost across the IdP round-trip and the callback defaults to DotSamlConstants.DEFAULT_LOGIN_PATH = /dotAdmin/:
- The RelayState dotCMS sends is built from the
auth.relaystate velocity template; if not configured (the default), an empty RelayState is sent, so DotSamlResource.processLogin gets RelayState == null.
- The session fallback (
REDIRECT_AFTER_LOGIN) is also null on the callback because the session doesn't survive the cross-site IdP POST (SameSite cookies → fresh session).
- With no
redirect.after.login config set, it defaults to /dotAdmin/ → a front-end-only user loops (now caught by the loop-guard as a 403, but they still don't reach their page).
Proposed fix: default the RelayState to the original request URI (ORIGINAL_REQUEST) when no auth.relaystate template is configured, so the return URL survives the round-trip via the SAML RelayState param (which the IdP echoes reliably).
Security requirement: DotSamlResource.processLogin currently redirects straight to the RelayState value (RedirectUtil.sendRedirectHTML(resp, loginPath)). Before honoring an auto-derived RelayState we must add open-redirect validation — only allow relative / same-host paths — otherwise it becomes a redirect vulnerability.
Needs a small design pass + testing across IdPs (Entra ID, Okta).
Immediate workaround (no code): set redirect.after.login (or an auth.relaystate template) on the SAML app to a front-end URL so front-end users land there instead of /dotAdmin/.
2. Fix logout redirect fall-through (spurious DoesNotExistException)
DotSamlResource.logoutGet and logoutPost do not return after RedirectUtil.sendRedirectHTML(...), so after sending the redirect they fall through and always throw new DoesNotExistException("No idpConfig …") — even when the idpConfig was found and the redirect succeeded. The redirect still works (response already committed) but it logs a misleading error every logout. Fix: add return; after the redirect in both methods.
Refs: #32365, #36541, #36590
Follow-up to #32365 / PR #36590 (which broke the infinite loop and added the default 403 "Access Denied" page + deterministic loop-guard). Two related improvements were deliberately deferred:
1. Return the user to the page they requested (default RelayState)
Today, when a front-end SAML user authenticates, the original front-end URL is lost across the IdP round-trip and the callback defaults to
DotSamlConstants.DEFAULT_LOGIN_PATH=/dotAdmin/:auth.relaystatevelocity template; if not configured (the default), an empty RelayState is sent, soDotSamlResource.processLogingetsRelayState == null.REDIRECT_AFTER_LOGIN) is also null on the callback because the session doesn't survive the cross-site IdP POST (SameSite cookies → fresh session).redirect.after.loginconfig set, it defaults to/dotAdmin/→ a front-end-only user loops (now caught by the loop-guard as a 403, but they still don't reach their page).Proposed fix: default the RelayState to the original request URI (
ORIGINAL_REQUEST) when noauth.relaystatetemplate is configured, so the return URL survives the round-trip via the SAML RelayState param (which the IdP echoes reliably).Security requirement:
DotSamlResource.processLogincurrently redirects straight to the RelayState value (RedirectUtil.sendRedirectHTML(resp, loginPath)). Before honoring an auto-derived RelayState we must add open-redirect validation — only allow relative / same-host paths — otherwise it becomes a redirect vulnerability.Needs a small design pass + testing across IdPs (Entra ID, Okta).
Immediate workaround (no code): set
redirect.after.login(or anauth.relaystatetemplate) on the SAML app to a front-end URL so front-end users land there instead of/dotAdmin/.2. Fix logout redirect fall-through (spurious
DoesNotExistException)DotSamlResource.logoutGetandlogoutPostdo notreturnafterRedirectUtil.sendRedirectHTML(...), so after sending the redirect they fall through and alwaysthrow new DoesNotExistException("No idpConfig …")— even when the idpConfig was found and the redirect succeeded. The redirect still works (response already committed) but it logs a misleading error every logout. Fix: addreturn;after the redirect in both methods.Refs: #32365, #36541, #36590