Author: jleroux
Date: Tue Feb  6 13:21:34 2018
New Revision: 1823325

URL: http://svn.apache.org/viewvc?rev=1823325&view=rev
Log:
"Applied fix from trunk for revision: 1823324  " 
------------------------------------------------------------------------
r1823324 | jleroux | 2018-02-06 14:17:57 +0100 (mar., 06 févr. 2018) | 26 lines

Fixed: Security issue in Token Based Authentication
(OFBIZ-10206)

The version I commitetd so far in OFBIZ-9833 has a small security issue.
See the Jira description for all details

To test I have attached a OFBIZ-10206-external-server-test-example.patch to 
the Jira

This removes the external-server-query property now useless

In  ContextFilter the getHeader (wrapper) now uses an autoLoginCookie to get 
the userLoginId passed in the JWT instead of externalServerUserLogin parameter.
A sourceServerWebappName parameter must be passed from the client request to
allow reading the autoLoginCookie.

This userLoginId is then retrieved on the target server from the JWT in the 
externalServerLoginCheck which is simplified.

In LoginWorker
  getAutoLoginCookieName() has now 2 versions to allow to pass a webappname

  A new autoLogoutFromAllBackendSessions() method has been added but for now 
  commented out. Decommenting it out will be submitted as a patch in OFBIZ-4959.

Thanks: Leila Mekika for reporting the security issue directly to me
------------------------------------------------------------------------

Removed:
    
ofbiz/ofbiz-framework/branches/release17.12/framework/common/groovyScripts/ExternalServerName.groovy
Modified:
    ofbiz/ofbiz-framework/branches/release17.12/   (props changed)
    
ofbiz/ofbiz-framework/branches/release17.12/framework/security/config/security.properties
    
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
    
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
    
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java

Propchange: ofbiz/ofbiz-framework/branches/release17.12/
------------------------------------------------------------------------------
--- svn:mergeinfo (original)
+++ svn:mergeinfo Tue Feb  6 13:21:34 2018
@@ -10,4 +10,4 @@
 /ofbiz/branches/json-integration-refactoring:1634077-1635900
 /ofbiz/branches/multitenant20100310:921280-927264
 /ofbiz/branches/release13.07:1547657
-/ofbiz/ofbiz-framework/trunk:1819499,1819598,1819800,1819805,1819811,1820038,1820262,1820374-1820375,1820441,1820457,1820644,1820658,1820790,1820823,1820949,1820966,1821012,1821036,1821112,1821115,1821144,1821186,1821219,1821226,1821230,1821386,1821600,1821613,1821628,1821965,1822125,1822310,1822377,1822383,1822393,1822882
+/ofbiz/ofbiz-framework/trunk:1819499,1819598,1819800,1819805,1819811,1820038,1820262,1820374-1820375,1820441,1820457,1820644,1820658,1820790,1820823,1820949,1820966,1821012,1821036,1821112,1821115,1821144,1821186,1821219,1821226,1821230,1821386,1821600,1821613,1821628,1821965,1822125,1822310,1822377,1822383,1822393,1822882,1823324

Modified: 
ofbiz/ofbiz-framework/branches/release17.12/framework/security/config/security.properties
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/branches/release17.12/framework/security/config/security.properties?rev=1823325&r1=1823324&r2=1823325&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/branches/release17.12/framework/security/config/security.properties
 (original)
+++ 
ofbiz/ofbiz-framework/branches/release17.12/framework/security/config/security.properties
 Tue Feb  6 13:21:34 2018
@@ -137,9 +137,7 @@ security.login.externalLoginKey.enabled=
 # -- If true, then it's possible to connect to another webapp on another 
server w/o signing in
 # -- This needs to be changed on both the source server and the target server
 use-external-server=N
-# -- Name of the external server (DNS)  
-external-server-name=demo-trunk.ofbiz.apache.org
-# -- Query part of the URL to use
-external-server-query=/catalog/control/
+# -- Name of the external server (DNS) ex: demo-trunk.ofbiz.apache.org where 
the port is not needed, or localhost:8443 (default) for local tests (not using 
the same webapp)
+external-server-name=localhost:8443
 # -- Time To Live of the token send to the external server in seconds
 external-server-token-duration=30

Modified: 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java?rev=1823325&r1=1823324&r2=1823325&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
 (original)
+++ 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
 Tue Feb  6 13:21:34 2018
@@ -191,19 +191,23 @@ public class ContextFilter implements Fi
         HttpServletRequestWrapper wrapper = new 
HttpServletRequestWrapper(httpRequest) {
             @Override
             public String getHeader(String name) {
-                String externalServerUserLoginId = 
request.getParameter(ExternalLoginKeysManager.EXTERNAL_SERVER_LOGIN_KEY);
+                String sourceWebappName = 
request.getParameter(ExternalLoginKeysManager.SOURCE_SERVER_WEBAPP_NAME);
                 String value = null;
-                if (externalServerUserLoginId != null) {
-                    // ExternalLoginKeysManager .createJwt() arguments in 
order:
-                    // id an Id, I suggest userLoginId
-                    // issuer is who/what issued the token. I suggest the 
server DNS
-                    // subject is the subject of the token. I suggest the 
destination webapp
-                    // timeToLive is the token maximum duration
-                    String webAppName = 
UtilHttp.getApplicationName(httpRequest);
-                    String dnsName = 
ExternalLoginKeysManager.getExternalServerName(httpRequest);
-                    long timeToLive = 
ExternalLoginKeysManager.getJwtTokenTimeToLive(httpRequest);
-                    // We would need a Bearer token (in Authorization request 
header) if we were using Oauth2, here we don't, so no Bearer 
-                    value = 
ExternalLoginKeysManager.createJwt(externalServerUserLoginId, dnsName, 
webAppName , timeToLive);
+                if (sourceWebappName != null) {
+                    HttpServletRequest httpRequest = (HttpServletRequest) 
request;
+                    String userLoginId = 
LoginWorker.getAutoUserLoginId(httpRequest, sourceWebappName);
+                    if (userLoginId != null) { // At this stage the user must 
be logged in. But safer to check because we can't grab it from the session here.
+                            // ExternalLoginKeysManager.createJwt() arguments 
in order:
+                            // id an Id, here userLoginId
+                            // issuer is who/what issued the token, here the 
server URL
+                            // subject is the subject of the token, here the 
target webapp
+                            // timeToLive is the token maximum duration, 
default 30 seconds
+                            String targetWebAppName = 
UtilHttp.getApplicationName(httpRequest);
+                            String targetServerUrl = 
ExternalLoginKeysManager.getTargetServerUrl(httpRequest);
+                            long timeToLive = 
ExternalLoginKeysManager.getJwtTokenTimeToLive(httpRequest);
+                            // We would need a Bearer token (in Authorization 
request header) if we were using Oauth2, here we don't, so no Bearer 
+                            value = 
ExternalLoginKeysManager.createJwt(userLoginId, targetServerUrl, 
targetWebAppName , timeToLive);
+                    }
                 }
                 if (value != null) return value;
                 return super.getHeader(name);

Modified: 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java?rev=1823325&r1=1823324&r2=1823325&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
 (original)
+++ 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
 Tue Feb  6 13:21:34 2018
@@ -43,9 +43,13 @@ import org.apache.ofbiz.service.LocalDis
 import org.apache.ofbiz.webapp.WebAppUtil;
 
 import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.JwtBuilder;
 import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
 import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.UnsupportedJwtException;
 
 /**
  * This class manages the authentication tokens that provide single sign-on 
authentication to the OFBiz applications.
@@ -55,7 +59,7 @@ public class ExternalLoginKeysManager {
     private static final String EXTERNAL_LOGIN_KEY_ATTR = "externalLoginKey";
     // This Map is keyed by the randomly generated externalLoginKey and the 
value is a UserLogin GenericValue object
     private static final Map<String, GenericValue> externalLoginKeys = new 
ConcurrentHashMap<>();
-    public static final String EXTERNAL_SERVER_LOGIN_KEY = 
"externalServerLoginKey";
+    public static final String SOURCE_SERVER_WEBAPP_NAME = 
"sourceServerWebappName";
     // This works the same way than externalLoginKey but between 2 servers, 
not 2 webapps on the same server. 
     // The Single Sign On (SSO) is ensured by a JWT token, then all is handled 
as normal by a session on the reached server. 
     // The servers may or may not share a database but the 2 loginUserIds must 
be the same.
@@ -175,23 +179,37 @@ public class ExternalLoginKeysManager {
     }
     
     public static String externalServerLoginCheck(HttpServletRequest request, 
HttpServletResponse response) {
-
         Delegator delegator = (Delegator) request.getAttribute("delegator");
         HttpSession session = request.getSession();
 
-        String externalServerUserLoginId = 
request.getParameter(EXTERNAL_SERVER_LOGIN_KEY);
-        if (externalServerUserLoginId == null) return "success"; // Nothing to 
do here
-        if (!"Y".equals(EntityUtilProperties.getPropertyValue("security", 
"use-external-server", "N", delegator))) return "success"; // The target server 
does not allow external login by default
-
-        GenericValue currentUserLogin = (GenericValue) 
session.getAttribute("userLogin");
+        // The target server does not allow external login by default
+        boolean useExternalServer = 
"Y".equals(EntityUtilProperties.getPropertyValue("security", 
"use-external-server", "N", delegator));
+        String sourceWebappName = 
request.getParameter(SOURCE_SERVER_WEBAPP_NAME); 
+        if (!useExternalServer || sourceWebappName == null) return "success"; 
// Nothing to do here
 
         try {
-            GenericValue userLogin = 
EntityQuery.use(delegator).from("UserLogin").where("userLoginId", 
externalServerUserLoginId).queryOne();
+            String userLoginId = null;
+            String authorizationHeader = request.getHeader("Authorization");
+            if (authorizationHeader != null) {
+                Claims claims = returnsClaims(authorizationHeader);
+                userLoginId = getSourceUserLoginId(claims );
+                boolean jwtOK = checkJwt(authorizationHeader, userLoginId, 
getTargetServerUrl(request), UtilHttp.getApplicationName(request));
+                if (!jwtOK) {
+                    // Something unexpected happened here
+                    Debug.logWarning("*** There was a problem with the JWT 
token, not signin in the user login " + userLoginId, module);
+                    return "success";
+                }
+            } else {
+                // Nothing to do here
+                return "success";
+            }
+
+            
+            GenericValue userLogin = 
EntityQuery.use(delegator).from("UserLogin").where("userLoginId", 
userLoginId).queryOne();
             if (userLogin != null) {
-                //to check it's the right tenant
-                //in case username and password are the same in different 
tenants
+                // Check it's the right tenant in case username and password 
are the same in different tenants
+                // TODO : not sure this is really useful in the case of 
external server, should not hurt anyway
                 LocalDispatcher dispatcher = (LocalDispatcher) 
request.getAttribute("dispatcher");
-                delegator = (Delegator) request.getAttribute("delegator");
                 String oldDelegatorName = delegator.getDelegatorName();
                 ServletContext servletContext = session.getServletContext();
                 if 
(!oldDelegatorName.equals(userLogin.getDelegator().getDelegatorName())) {
@@ -199,44 +217,15 @@ public class ExternalLoginKeysManager {
                     dispatcher = 
WebAppUtil.makeWebappDispatcher(servletContext, delegator);
                     LoginWorker.setWebContextObjects(request, response, 
delegator, dispatcher);
                 }
-
-                String authorizationHeader = 
request.getHeader("Authorization");
-                if (authorizationHeader != null) {
-                    boolean jwtOK = checkJwt(authorizationHeader, 
userLogin.getString("userLoginId"), getExternalServerName(request), 
UtilHttp.getApplicationName(request));
-                    if (!jwtOK) {
-                        Debug.logWarning("*** There was a problem with the JWT 
token, loging out the current user: " + externalServerUserLoginId, module);
-                        LoginWorker.logout(request, response);
-                        return "success";
-                    }
-                } else {
-                    // Something weird happened here => logout current user
-                    Debug.logWarning("*** There was a problem with the JWT 
token, loging out the current user: " + externalServerUserLoginId, module);
-                    LoginWorker.logout(request, response);
-                    return "success";
-                }
-
-                // if the user is already logged in and the login is 
different, logout the other user
-                if (currentUserLogin != null) {
-                    if 
(currentUserLogin.getString("userLoginId").equals(userLogin.getString("userLoginId")))
 {
-                        // is the same user, just carry on...
-                        return "success";
-                    }
-
-                    // logout the current user and login the new user...
-                    LoginWorker.logout(request, response);
-                    // ignore the return value; even if the operation failed 
we want to set the new UserLogin
-                }
-
-                //connect
                 String enabled = userLogin.getString("enabled");
                 if (enabled == null || "Y".equals(enabled)) {
                     userLogin.set("hasLoggedOut", "N");
                     userLogin.store();
                 }
-                LoginWorker.doBasicLogin(userLogin, request);
             } else {
-                Debug.logWarning("Could not find userLogin for external login 
key: " + externalServerUserLoginId, module);
+                Debug.logWarning("*** There was a problem with the JWT token. 
Could not find userLogin " + userLoginId, module);
             }
+            LoginWorker.doBasicLogin(userLogin, request);
         } catch (GenericEntityException e) {
             Debug.logError(e, "Cannot get autoUserLogin information: " + 
e.getMessage(), module);
         }
@@ -264,11 +253,11 @@ public class ExternalLoginKeysManager {
         Key signingKey = new SecretKeySpec(apiKeySecretBytes, 
signatureAlgorithm.getJcaName());
         //Let's set the JWT Claims
         JwtBuilder builder = Jwts.builder().setId(id)
-                                    .setIssuedAt(now)
-                                    .setSubject(subject)
-                                    .setIssuer(issuer)
-                                    .setIssuedAt(now)
-                                    .signWith(signatureAlgorithm, signingKey);
+                .setIssuedAt(now)
+                .setSubject(subject)
+                .setIssuer(issuer)
+                .setIssuedAt(now)
+                .signWith(signatureAlgorithm, signingKey);
 
         //if it has been specified, let's add the expiration date, this should 
always be true
         if (ttlMillis >= 0) {
@@ -292,6 +281,28 @@ public class ExternalLoginKeysManager {
      */
     private static boolean checkJwt(String jwt, String id, String issuer, 
String subject) {
         //The JWT signature algorithm is using this to sign the token
+        Claims claims = returnsClaims(jwt);
+
+        long nowMillis = System.currentTimeMillis();
+        Date now = new Date(nowMillis);
+
+        return claims.getId().equals(id) 
+                && claims.getIssuer().equals(issuer)
+                && claims.getSubject().equals(subject)
+                && claims.getExpiration().after(now);
+    }
+
+    /**
+     * @param jwt a JWT token
+     * @return claims the claims
+     * @throws ExpiredJwtException
+     * @throws UnsupportedJwtException
+     * @throws MalformedJwtException
+     * @throws SignatureException
+     * @throws IllegalArgumentException
+     */
+    private static Claims returnsClaims(String jwt) throws 
ExpiredJwtException, UnsupportedJwtException,
+            MalformedJwtException, SignatureException, 
IllegalArgumentException {
         SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
 
         byte[] apiKeySecretBytes = 
DatatypeConverter.parseBase64Binary(ExternalServerJwtMasterSecretKey);
@@ -301,25 +312,21 @@ public class ExternalLoginKeysManager {
         Claims claims = Jwts.parser()
            .setSigningKey(signingKey)
            .parseClaimsJws(jwt).getBody();
-
-        long nowMillis = System.currentTimeMillis();
-        Date now = new Date(nowMillis);
-
-        return claims.getId().equals(id) 
-                && claims.getIssuer().equals(issuer)
-                && claims.getSubject().equals(subject)
-                && claims.getExpiration().after(now);
+        return claims;
     }
 
-    public static String getExternalServerName(HttpServletRequest request) {
-        String reportingServerName = "";
+    private static String getSourceUserLoginId(Claims claims) {
+        return claims.getId();
+    }
+    
+    public static String getTargetServerUrl(HttpServletRequest request) {
+        String targetServerUrl = "";
         Delegator delegator = (Delegator) request.getAttribute("delegator");
         if (delegator != null && 
"Y".equals(EntityUtilProperties.getPropertyValue("security", 
"use-external-server", "N", delegator))) {
-            reportingServerName = 
EntityUtilProperties.getPropertyValue("security", "external-server-name", 
"localhost:8443", delegator);
-            String reportingServerQuery = 
EntityUtilProperties.getPropertyValue("security", "external-server-query", 
"/catalog/control/", delegator);
-            reportingServerName = "https://"; + reportingServerName + 
reportingServerQuery;
+            targetServerUrl = 
EntityUtilProperties.getPropertyValue("security", "external-server-name", 
"localhost:8443", delegator);
+            targetServerUrl = "https://"; + targetServerUrl;
         }
-        return reportingServerName;
+        return targetServerUrl;
     }
     
     public static long getJwtTokenTimeToLive(HttpServletRequest request) {

Modified: 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java?rev=1823325&r1=1823324&r2=1823325&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
 (original)
+++ 
ofbiz/ofbiz-framework/branches/release17.12/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java
 Tue Feb  6 13:21:34 2018
@@ -622,12 +622,13 @@ public class LoginWorker {
         RequestHandler rh = 
RequestHandler.getRequestHandler(request.getSession().getServletContext());
         rh.runBeforeLogoutEvents(request, response);
 
-
         // invalidate the security group list cache
         GenericValue userLogin = (GenericValue) 
request.getSession().getAttribute("userLogin");
 
         doBasicLogout(userLogin, request, response);
-
+        
+        //autoLogoutFromAllBackendSessions(userLogin, request, response);
+        // TODO check why, seems no sense
         if (request.getAttribute("_AUTO_LOGIN_LOGOUT_") == null) {
             return autoLoginCheck(request, response);
         }
@@ -718,6 +719,14 @@ public class LoginWorker {
         return UtilHttp.getApplicationName(request) + ".autoUserLoginId";
     }
 
+    protected static String getAutoLoginCookieName(String webappName) {
+        return webappName + ".autoUserLoginId";
+    }
+    
+    /**
+    * @deprecated Moved to {@link 
org.apache.ofbiz.webapp.control.LoginWorker#getAutoUserLoginId(HttpServletRequest
 request, String webappName) String}
+    */
+   @Deprecated
     public static String getAutoUserLoginId(HttpServletRequest request) {
         String autoUserLoginId = null;
         Cookie[] cookies = request.getCookies();
@@ -734,12 +743,31 @@ public class LoginWorker {
         }
         return autoUserLoginId;
     }
+    
+    public static String getAutoUserLoginId(HttpServletRequest request, String 
webappName) {
+        String autoUserLoginId = null;
+        Cookie[] cookies = request.getCookies();
+        if (Debug.verboseOn()) {
+            Debug.logVerbose("Cookies: " + Arrays.toString(cookies), module);
+        }
+        if (cookies != null) {
+            for (Cookie cookie: cookies) {
+                String cookieName = (webappName != null) ? 
getAutoLoginCookieName(webappName) : getAutoLoginCookieName(request);
+                if (cookie.getName().equals(cookieName)) {
+                    autoUserLoginId = cookie.getValue();
+                    break;
+                }
+            }
+        }
+        return autoUserLoginId;
+    }
+
 
     public static String autoLoginCheck(HttpServletRequest request, 
HttpServletResponse response) {
         Delegator delegator = (Delegator) request.getAttribute("delegator");
         HttpSession session = request.getSession();
 
-        return autoLoginCheck(delegator, session, getAutoUserLoginId(request));
+        return autoLoginCheck(delegator, session, getAutoUserLoginId(request, 
null));
     }
 
     private static String autoLoginCheck(Delegator delegator, HttpSession 
session, String autoUserLoginId) {
@@ -794,6 +822,34 @@ public class LoginWorker {
         return "success";
     }
 
+    public static String autoLogoutFromAllBackendSessions(GenericValue 
userLogin, HttpServletRequest request, HttpServletResponse response) {
+        HttpSession session = request.getSession();
+
+        // remove all the autoLoginCookies but if in ecommerce/ecomseo and 
webpos (it's done manually there, not sure for webpos TODO: check)
+        Cookie[] cookies = request.getCookies();
+        if (Debug.verboseOn()) {
+            Debug.logVerbose("Cookies: " + Arrays.toString(cookies), module);
+        }
+        if (cookies != null && userLogin != null) {
+            for (Cookie autoLoginCookie: cookies) {
+                if (autoLoginCookie.getName().contains("autoUserLoginId")
+                        && !(autoLoginCookie.getName().contains("ecommerce") 
+                        || autoLoginCookie.getName().contains("ecomseo") 
+                        || autoLoginCookie.getName().contains("webpos")))
+                autoLoginCookie.setMaxAge(0);
+                autoLoginCookie.setPath("/");
+                response.addCookie(autoLoginCookie);
+            }
+        }
+        
+        // remove the session attributes
+        session.removeAttribute("autoUserLogin");
+        session.removeAttribute("autoName");
+
+        request.setAttribute("_AUTO_LOGIN_LOGOUT_", Boolean.TRUE); // TODO 
check it's useful
+        return "success";
+    }
+
     public static boolean isUserLoggedIn(HttpServletRequest request) {
         HttpSession session = request.getSession();
         GenericValue currentUserLogin = (GenericValue) 
session.getAttribute("userLogin");


Reply via email to