This is an automated email from the ASF dual-hosted git repository.

mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 45e8de777f Improved: Bind externalLoginKey to its intended destination 
webapps instead of validating against any webapp on the server (OFBIZ-13522)
45e8de777f is described below

commit 45e8de777fc1f22d5050a9afe8c81e3056ffa82b
Author: Mridul Pathak <[email protected]>
AuthorDate: Fri Sep 4 13:22:33 2026 +0530

    Improved: Bind externalLoginKey to its intended destination webapps instead 
of validating against any webapp on the server (OFBIZ-13522)
    
    ExternalLoginTicket previously carried a nullable targetContextPath that 
every mint site left null, making isValidFor treat any webapp on the server as 
a valid redemption target for a captured key, not just the webapps the render 
that minted it actually linked to. It now carries a mutable allow-list of 
context paths that is grown as concrete destinations become known, at 
WidgetWorker#buildHyperlinkUri's inter-app branch, the duplicate inter-app 
branch in HtmlTreeRenderer#renderLink, a [...]
---
 .../webapp/control/ExternalLoginKeysManager.java   | 82 +++++++++++++++++++---
 .../ofbiz/webapp/control/RequestHandler.java       |  6 +-
 .../control/ExternalLoginKeysManagerTests.java     | 77 ++++++++++++++++++++
 .../java/org/apache/ofbiz/widget/WidgetWorker.java |  2 +
 .../ofbiz/widget/renderer/ScreenRenderer.java      | 31 ++++++++
 .../widget/renderer/html/HtmlTreeRenderer.java     |  2 +
 6 files changed, 190 insertions(+), 10 deletions(-)

diff --git 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
index 49dc13f6a9..4c32567b36 100644
--- 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
+++ 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
@@ -28,6 +28,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
 
 import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.entity.Delegator;
 import org.apache.ofbiz.entity.DelegatorFactory;
 import org.apache.ofbiz.entity.GenericValue;
@@ -55,23 +56,28 @@ public class ExternalLoginKeysManager {
 
     /**
      * A minted external login key, bound to the UserLogin it authenticates 
and to a deadline.
-     * Optionally bound to a target context path too, for mint sites that know 
which webapp the
-     * key is destined for; existing mint sites do not, so that field is null 
and the check is
-     * skipped for them.
+     * Also bound to the set of destination webapps it is actually attached 
to: a mint site does
+     * not know in advance every webapp its render will link to (one render 
mints one key and
+     * hands it to several cross-webapp links, possibly several different 
destinations), so
+     * destinations are registered as they become known -- see #allowFor -- 
rather than fixed at
+     * construction time. A ticket with no registered destinations yet 
authenticates nowhere.
      */
     private static final class ExternalLoginTicket {
         private final GenericValue userLogin;
-        private final String targetContextPath;
         private final long expiresAtMillis;
+        // Context paths this ticket is allowed to authenticate into. Grown by 
#allowFor as the
+        // concrete destinations of this render's cross-webapp links become 
known; checked by
+        // #isValidFor so a leaked key is only ever redeemable against a 
webapp this render
+        // actually intended to reach, not any webapp on the server.
+        private final Set<String> allowedContextPaths = 
ConcurrentHashMap.newKeySet();
         // Context paths this ticket has already been redeemed for. A single 
render shares one
         // key across every cross-webapp link it emits, so the same key 
legitimately needs to
         // authenticate into several different webapps; this set is what stops 
it authenticating
         // into the *same* webapp twice, which is the actual replay this 
ticket must prevent.
         private final Set<String> redeemedContextPaths = 
ConcurrentHashMap.newKeySet();
 
-        ExternalLoginTicket(GenericValue userLogin, String targetContextPath) {
+        ExternalLoginTicket(GenericValue userLogin) {
             this.userLogin = userLogin;
-            this.targetContextPath = targetContextPath;
             this.expiresAtMillis = System.currentTimeMillis() + 
EXTERNAL_LOGIN_KEY_TTL_MILLIS;
         }
 
@@ -79,11 +85,19 @@ public class ExternalLoginKeysManager {
             return userLogin;
         }
 
+        /**
+         * Allows this ticket to authenticate into the given destination 
webapp.
+         * @param contextPath the destination webapp's context path, e.g. 
"/partymgr"
+         */
+        void allowFor(String contextPath) {
+            allowedContextPaths.add(contextPath);
+        }
+
         boolean isValidFor(HttpServletRequest request) {
             if (System.currentTimeMillis() > expiresAtMillis) {
                 return false;
             }
-            return targetContextPath == null || 
targetContextPath.equals(request.getServletContext().getContextPath());
+            return 
allowedContextPaths.contains(request.getServletContext().getContextPath());
         }
 
         /**
@@ -131,7 +145,7 @@ public class ExternalLoginKeysManager {
 
             request.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey);
             session.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey);
-            EXTERNAL_LOGIN_KEYS.put(externalKey, new 
ExternalLoginTicket(userLogin, null));
+            EXTERNAL_LOGIN_KEYS.put(externalKey, new 
ExternalLoginTicket(userLogin));
             return externalKey;
         }
     }
@@ -147,6 +161,58 @@ public class ExternalLoginKeysManager {
         }
     }
 
+    /**
+     * Allows an already-minted key to authenticate into a destination webapp, 
given that
+     * webapp's context path directly. Used at the point a concrete 
destination is already
+     * known, e.g. the set of webapps a rendered app-switcher menu can link to.
+     * @param externalLoginKey the minted key, as returned by {@link 
#getExternalLoginKey}
+     * @param contextPath the destination webapp's context path, e.g. 
"/partymgr"
+     */
+    public static void registerDestination(String externalLoginKey, String 
contextPath) {
+        if (UtilValidate.isEmpty(externalLoginKey) || 
UtilValidate.isEmpty(contextPath)) {
+            return;
+        }
+        ExternalLoginTicket ticket = EXTERNAL_LOGIN_KEYS.get(externalLoginKey);
+        if (ticket != null) {
+            ticket.allowFor(contextPath);
+        }
+    }
+
+    /**
+     * Allows an already-minted key to authenticate into the destination 
webapp targeted by a
+     * relative inter-app link, e.g. {@code 
"/partymgr/control/viewprofile?partyId=10000"} or
+     * {@code "/partymgr/control"}. The destination webapp's context path is 
parsed out of the
+     * target; a target that isn't a routed control-servlet request registers 
nothing, since
+     * nothing would ever redeem a ticket against it anyway.
+     * @param externalLoginKey the minted key, as returned by {@link 
#getExternalLoginKey}
+     * @param interAppTarget the relative target of an inter-app link
+     */
+    public static void registerInterAppDestination(String externalLoginKey, 
String interAppTarget) {
+        registerDestination(externalLoginKey, contextRootOf(interAppTarget));
+    }
+
+    /**
+     * Parses the destination webapp's context path out of a relative 
inter-app target, by
+     * taking everything before the "/control" path segment.
+     * @param target the relative target, e.g. 
"/partymgr/control/viewprofile?partyId=10000"
+     * @return the context path, e.g. "/partymgr" (possibly empty, for a 
root-mounted webapp),
+     *     or null if the target has no "/control" path segment to anchor on
+     */
+    private static String contextRootOf(String target) {
+        if (target == null) {
+            return null;
+        }
+        int idx = target.indexOf("/control");
+        while (idx >= 0) {
+            int afterIdx = idx + "/control".length();
+            if (afterIdx == target.length() || target.charAt(afterIdx) == '/' 
|| target.charAt(afterIdx) == '?') {
+                return target.substring(0, idx);
+            }
+            idx = target.indexOf("/control", idx + 1);
+        }
+        return null;
+    }
+
     /**
      * OFBiz controller event that performs the user authentication using the 
authentication token.
      * The method is designed to be used in a chain of controller preprocessor 
event: it always return "success"
diff --git 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
index 1aa558bcf8..4dc7024a25 100644
--- 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
+++ 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
@@ -1535,10 +1535,12 @@ public final class RequestHandler {
         }
 
         if (addExternalKeyParam) {
+            String externalLoginKey = 
ExternalLoginKeysManager.getExternalLoginKey(request);
+            
ExternalLoginKeysManager.registerInterAppDestination(externalLoginKey, 
targetControlPath);
             if (url.contains("?")) {
-                url += "&externalLoginKey=" + 
ExternalLoginKeysManager.getExternalLoginKey(request);
+                url += "&externalLoginKey=" + externalLoginKey;
             } else {
-                url += "?externalLoginKey=" + 
ExternalLoginKeysManager.getExternalLoginKey(request);
+                url += "?externalLoginKey=" + externalLoginKey;
             }
         }
 
diff --git 
a/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
 
b/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
index 2ac8fb7e26..c24f8bbfdd 100644
--- 
a/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
+++ 
b/framework/webapp/src/test/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManagerTests.java
@@ -123,6 +123,9 @@ public class ExternalLoginKeysManagerTests {
         when(mintRequest.getSession()).thenReturn(mintSession);
         when(mintRequest.getAttribute("userLogin")).thenReturn(userLogin);
         String key = ExternalLoginKeysManager.getExternalLoginKey(mintRequest);
+        // The mint site attaches the key to a concrete link, e.g. a partymgr 
menu item,
+        // registering that destination -- exactly as 
WidgetWorker/RequestHandler do.
+        ExternalLoginKeysManager.registerDestination(key, "/partymgr");
 
         // Both redemptions target the same webapp, so the second one is the 
actual replay case.
         ServletContext servletContext = mock(ServletContext.class);
@@ -165,4 +168,78 @@ public class ExternalLoginKeysManagerTests {
             loginWorker.verify(() -> LoginWorker.autoLoginSet(replay, 
replayResponse));
         }
     }
+
+    @Test
+    public void 
checkExternalLoginKeyRejectsRedemptionAgainstAnUnregisteredDestination() {
+        // Mint a key and register it against partymgr only -- e.g. a page 
that linked to
+        // partymgr, never to webtools.
+        GenericValue userLogin = mock(GenericValue.class);
+        HttpServletRequest mintRequest = mock(HttpServletRequest.class);
+        HttpSession mintSession = mock(HttpSession.class);
+        when(mintRequest.getSession()).thenReturn(mintSession);
+        when(mintRequest.getAttribute("userLogin")).thenReturn(userLogin);
+        String key = ExternalLoginKeysManager.getExternalLoginKey(mintRequest);
+        ExternalLoginKeysManager.registerDestination(key, "/partymgr");
+
+        // A captured key is presented against a webapp this render never 
linked to.
+        ServletContext webtoolsContext = mock(ServletContext.class);
+        when(webtoolsContext.getContextPath()).thenReturn("/webtools");
+        HttpServletRequest attempt = mock(HttpServletRequest.class);
+        HttpServletResponse attemptResponse = mock(HttpServletResponse.class);
+        when(attempt.getParameter("externalLoginKey")).thenReturn(key);
+        when(attempt.getServletContext()).thenReturn(webtoolsContext);
+
+        try (MockedStatic<LoginWorker> loginWorker = 
mockStatic(LoginWorker.class);
+                MockedStatic<EntityUtilProperties> props = 
mockStatic(EntityUtilProperties.class)) {
+            props.when(() -> EntityUtilProperties.getPropertyValue(
+                    "security", "security.login.externalLoginKey.enabled", 
"true", null)).thenReturn("true");
+
+            String result = 
ExternalLoginKeysManager.checkExternalLoginKey(attempt, attemptResponse);
+
+            assertEquals("success", result);
+            loginWorker.verify(() -> LoginWorker.doBasicLogin(any(), any(), 
any()), never());
+            loginWorker.verify(() -> LoginWorker.autoLoginSet(attempt, 
attemptResponse));
+        }
+    }
+
+    @Test
+    public void 
registerInterAppDestinationParsesContextRootFromControlServletTarget() {
+        Delegator delegator = mock(Delegator.class);
+        when(delegator.getDelegatorName()).thenReturn("default");
+        GenericValue userLogin = mock(GenericValue.class);
+        when(userLogin.getDelegator()).thenReturn(delegator);
+        when(userLogin.getString("userLoginId")).thenReturn("demoadmin");
+
+        HttpServletRequest mintRequest = mock(HttpServletRequest.class);
+        HttpSession mintSession = mock(HttpSession.class);
+        when(mintRequest.getSession()).thenReturn(mintSession);
+        when(mintRequest.getAttribute("userLogin")).thenReturn(userLogin);
+        String key = ExternalLoginKeysManager.getExternalLoginKey(mintRequest);
+
+        // As WidgetWorker/HtmlTreeRenderer see it: a relative inter-app link 
target, not a
+        // bare context path.
+        ExternalLoginKeysManager.registerInterAppDestination(key, 
"/partymgr/control/viewprofile?partyId=10000");
+
+        ServletContext servletContext = mock(ServletContext.class);
+        when(servletContext.getContextPath()).thenReturn("/partymgr");
+        HttpServletRequest attempt = mock(HttpServletRequest.class);
+        HttpServletResponse attemptResponse = mock(HttpServletResponse.class);
+        HttpSession attemptSession = mock(HttpSession.class);
+        when(attempt.getParameter("externalLoginKey")).thenReturn(key);
+        when(attempt.getServletContext()).thenReturn(servletContext);
+        when(attempt.getAttribute("delegator")).thenReturn(delegator);
+        when(attempt.getSession()).thenReturn(attemptSession);
+
+        try (MockedStatic<LoginWorker> loginWorker = 
mockStatic(LoginWorker.class);
+                MockedStatic<EntityUtilProperties> props = 
mockStatic(EntityUtilProperties.class)) {
+            loginWorker.when(() -> LoginWorker.checkLogout(any(), 
any())).thenReturn(userLogin);
+            props.when(() -> EntityUtilProperties.getPropertyValue(
+                    "security", "security.login.externalLoginKey.enabled", 
"true", delegator)).thenReturn("true");
+
+            String result = 
ExternalLoginKeysManager.checkExternalLoginKey(attempt, attemptResponse);
+
+            assertEquals("success", result);
+            loginWorker.verify(() -> LoginWorker.doBasicLogin(userLogin, 
attempt, attemptResponse), times(1));
+        }
+    }
 }
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java
index e32037954d..4eeed077c7 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java
@@ -34,6 +34,7 @@ import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.entity.Delegator;
 import org.apache.ofbiz.service.LocalDispatcher;
 import org.apache.ofbiz.webapp.control.ConfigXMLReader;
+import org.apache.ofbiz.webapp.control.ExternalLoginKeysManager;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.webapp.taglib.ContentUrlTag;
 import org.apache.ofbiz.widget.model.CommonWidgetModels;
@@ -108,6 +109,7 @@ public final class WidgetWorker {
             if (!isAbsoluteTarget(localRequestName)) {
                 String externalLoginKey = (String) 
request.getAttribute("externalLoginKey");
                 additionalParameters.put("externalLoginKey", externalLoginKey);
+                
ExternalLoginKeysManager.registerInterAppDestination(externalLoginKey, 
localRequestName);
             }
         } else if ("content".equals(targetType)) {
             uriString = getContentUrl(localRequestName, request);
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java
index 72bf8bf2d4..a6ff3fbd70 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java
@@ -35,6 +35,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpSession;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.apache.ofbiz.base.component.ComponentConfig.WebappInfo;
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.GeneralException;
 import org.apache.ofbiz.base.util.UtilDateTime;
@@ -54,6 +55,7 @@ import org.apache.ofbiz.security.Security;
 import org.apache.ofbiz.service.DispatchContext;
 import org.apache.ofbiz.service.GenericServiceException;
 import org.apache.ofbiz.service.LocalDispatcher;
+import org.apache.ofbiz.webapp.WebAppCache;
 import org.apache.ofbiz.webapp.control.ExternalLoginKeysManager;
 import org.apache.ofbiz.webapp.control.LoginWorker;
 import org.apache.ofbiz.webapp.website.WebSiteWorker;
@@ -308,6 +310,12 @@ public class ScreenRenderer {
         boolean externalLoginKeyEnabled = 
ExternalLoginKeysManager.isExternalLoginKeyEnabled(request);
         if (externalLoginKeyEnabled) {
             externalLoginKey = 
ExternalLoginKeysManager.getExternalLoginKey(request);
+            // Themes build the app-switcher menu directly in FTL 
(string-concatenated hrefs,
+            // bypassing WidgetWorker/RequestHandler entirely), so those 
destinations can never be
+            // registered at the point the key is embedded. Pre-register them 
here instead, using
+            // the same lookup the templates themselves use, so a key exposed 
to a template is
+            // only ever valid for the webapps that app-switcher can actually 
link to.
+            registerAppBarDestinations(externalLoginKey, servletContext);
         }
         String externalKeyParam = externalLoginKey == null ? "" : 
"&amp;externalLoginKey=" + externalLoginKey;
         context.put("externalLoginKey", externalLoginKey);
@@ -368,6 +376,29 @@ public class ScreenRenderer {
         context.push();
     }
 
+    /**
+     * Allows the given key to authenticate into every webapp the app-switcher 
menu (built
+     * directly in theme FTL templates, from this same lookup) can link to for 
the current
+     * server, so the app-switcher keeps working under the destination-scoped 
externalLoginKey.
+     * @param externalLoginKey the minted key, as returned by {@link 
ExternalLoginKeysManager#getExternalLoginKey}
+     * @param servletContext the current webapp's servlet context, used to 
find the running server's id
+     */
+    private static void registerAppBarDestinations(String externalLoginKey, 
ServletContext servletContext) {
+        if (servletContext == null) {
+            return;
+        }
+        String serverId = (String) servletContext.getAttribute("_serverId");
+        if (serverId == null) {
+            return;
+        }
+        WebAppCache webAppCache = WebAppCache.getShared();
+        for (String menuName : UtilMisc.toList("main", "secondary")) {
+            for (WebappInfo webappInfo : 
webAppCache.getAppBarWebInfos(serverId, menuName)) {
+                ExternalLoginKeysManager.registerDestination(externalLoginKey, 
webappInfo.getContextRoot());
+            }
+        }
+    }
+
     /**
      * Gets context.
      * @return the context
diff --git 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
index 341e157f85..fe68c63fb2 100644
--- 
a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
+++ 
b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/html/HtmlTreeRenderer.java
@@ -30,6 +30,7 @@ import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.StringUtil;
 import org.apache.ofbiz.base.util.UtilGenerics;
 import org.apache.ofbiz.base.util.UtilValidate;
+import org.apache.ofbiz.webapp.control.ExternalLoginKeysManager;
 import org.apache.ofbiz.webapp.control.RequestHandler;
 import org.apache.ofbiz.webapp.taglib.ContentUrlTag;
 import org.apache.ofbiz.widget.WidgetWorker;
@@ -259,6 +260,7 @@ public class HtmlTreeRenderer extends HtmlWidgetRenderer 
implements TreeStringRe
                         writer.append("?externalLoginKey=");
                     }
                     writer.append(externalLoginKey);
+                    
ExternalLoginKeysManager.registerInterAppDestination(externalLoginKey, target);
                 }
             } else {
                 writer.append(target);

Reply via email to