Author: jleroux
Date: Sun Oct 29 11:02:00 2017
New Revision: 1813679

URL: http://svn.apache.org/viewvc?rev=1813679&view=rev
Log:
Implemented: Token Based Authentication
(OFBIZ-9833)

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 loginUserIds on the 2 servers must be the same.

OOTB the JWT masterSecretKey is not properly initialised and can not be OOTB.
As we sign on on several servers, so have different sessions, we can't use the 
externalLoginKey way to create the JWT masterSecretKey.
The best way to create the JWT masterSecretKey is to use a temporary way to 
load 
in a static final key when compiling. This is simple and most secure. 
One of the proposed way is to use sed and uuidgen to modify the masterSecretKey 
value. The magic words here are TEMPORARY and FINAL!

I have not tested this between 2 servers yet, only locally where it works well.
I'll do after this commit between my local instance and the trunk demo.

Thanks: Nicolas for the sed ans uuidgen suggestion

Added:
    
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
   (with props)
Modified:
    ofbiz/ofbiz-framework/trunk/build.gradle
    
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
    
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java

Modified: ofbiz/ofbiz-framework/trunk/build.gradle
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/build.gradle?rev=1813679&r1=1813678&r2=1813679&view=diff
==============================================================================
--- ofbiz/ofbiz-framework/trunk/build.gradle (original)
+++ ofbiz/ofbiz-framework/trunk/build.gradle Sun Oct 29 11:02:00 2017
@@ -141,6 +141,10 @@ dependencies {
     compile 'org.zapodot:jackson-databind-java-optional:2.6.1'
     compile 'oro:oro:2.0.8'
     compile 'wsdl4j:wsdl4j:1.6.3'
+    compile 'io.jsonwebtoken:jjwt:0.9.0'
+    compile 'com.fasterxml.jackson.core:jackson-core:2.7.3'
+    compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.3'
+    compile 'com.fasterxml.jackson.core:jackson-databind:2.7.3'
 
     // ofbiz unit-test compile libs
     testCompile 'org.mockito:mockito-core:2.+'

Added: 
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy?rev=1813679&view=auto
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
 (added)
+++ 
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
 Sun Oct 29 11:02:00 2017
@@ -0,0 +1,19 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+context.reportingServer = 
org.apache.ofbiz.webapp.control.ExternalLoginKeysManager.getExternalServerName(request)
 

Propchange: 
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: 
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
------------------------------------------------------------------------------
    svn:keywords = Date Rev Author URL Id

Propchange: 
ofbiz/ofbiz-framework/trunk/framework/common/groovyScripts/ExternalServerName.groovy
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: 
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java?rev=1813679&r1=1813678&r2=1813679&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
 (original)
+++ 
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ContextFilter.java
 Sun Oct 29 11:02:00 2017
@@ -18,11 +18,8 @@
  
*******************************************************************************/
 package org.apache.ofbiz.webapp.control;
 
-import static org.apache.ofbiz.base.util.UtilGenerics.checkMap;
-
 import java.io.IOException;
 import java.util.Enumeration;
-import java.util.Map;
 
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -31,13 +28,12 @@ import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
 
 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.UtilHttp;
-import org.apache.ofbiz.base.util.UtilObject;
 import org.apache.ofbiz.base.util.UtilValidate;
 import org.apache.ofbiz.entity.Delegator;
 import org.apache.ofbiz.entity.DelegatorFactory;
@@ -192,8 +188,29 @@ 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 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 Authorisation request 
header) if we were using Oauth2, here we don't, so no Bearer 
+                    value = 
ExternalLoginKeysManager.createJwt(externalServerUserLoginId, dnsName, 
webAppName , timeToLive);
+                }
+                if (value != null) return value;
+                return super.getHeader("Authorisation");
+            }
+        };
         // we're done checking; continue on
-        chain.doFilter(request, httpResponse);
+        chain.doFilter(wrapper, httpResponse);
     }
 
     /**

Modified: 
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java?rev=1813679&r1=1813678&r2=1813679&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
 (original)
+++ 
ofbiz/ofbiz-framework/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java
 Sun Oct 29 11:02:00 2017
@@ -18,21 +18,34 @@
  */
 package org.apache.ofbiz.webapp.control;
 
+import java.security.Key;
+import java.util.Date;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 
+import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
+import javax.xml.bind.DatatypeConverter;
 
 import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.UtilHttp;
 import org.apache.ofbiz.entity.Delegator;
 import org.apache.ofbiz.entity.DelegatorFactory;
+import org.apache.ofbiz.entity.GenericEntityException;
 import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.entity.util.EntityUtilProperties;
 import org.apache.ofbiz.service.LocalDispatcher;
 import org.apache.ofbiz.webapp.WebAppUtil;
 
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtBuilder;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
 /**
  * This class manages the authentication tokens that provide single sign-on 
authentication to the OFBiz applications.
  */
@@ -41,6 +54,17 @@ 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";
+    // 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 loginUserId must 
be the same.
+    
+    // OOTB the JWT masterSecretKey is not properly initialised and can not be 
OOTB.
+    // As we sign on on several servers, so have different sessions, we can't 
use the externalLoginKey way to create the JWT masterSecretKey.
+    // The best way to create the JWT masterSecretKey is to use a temporary 
way to load in a static final key when compiling. 
+    // This is simple and most secure. One of the proposed way is to use sed 
and uuidgen to modify the masterSecretKey value
+    // The magic words here are TEMPORARY and FINAL!
+    private static final String ExternalServerJwtMasterSecretKey = 
"ExternalServerJwtMasterSecretKey";
 
     /**
      * Gets (and creates if necessary) an authentication token to be used for 
an external login parameter.
@@ -146,5 +170,158 @@ public class ExternalLoginKeysManager {
     private static boolean isAjax(HttpServletRequest request) {
        return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
     }
+    
+    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
+
+        GenericValue currentUserLogin = (GenericValue) 
session.getAttribute("userLogin");
+
+        try {
+            GenericValue userLogin = delegator.findOne("UserLogin", false, 
"userLoginId", externalServerUserLoginId);
+            if (userLogin != null) {
+                //to check it's the right tenant
+                //in case username and password are the same in different 
tenants
+                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())) {
+                    delegator = 
DelegatorFactory.getDelegator(userLogin.getDelegator().getDelegatorName());
+                    dispatcher = 
WebAppUtil.makeWebappDispatcher(servletContext, delegator);
+                    LoginWorker.setWebContextObjects(request, response, 
delegator, dispatcher);
+                }
+
+                String authorisationHeader = 
request.getHeader("Authorisation");
+                if (authorisationHeader != null) {
+                    boolean jwtOK = checkJwt(authorisationHeader, 
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);
+            }
+        } catch (GenericEntityException e) {
+            Debug.logError(e, "Cannot get autoUserLogin information: " + 
e.getMessage(), module);
+        }
+
+        return "success";
+    }
+    
+    /**
+     * Generate and return a JWT key
+     * 
+     * @param id is an Id, I suggest userLoginId
+     * @param issuer is who/what issued the token. I suggest the server DNS
+     * @param subject is the subject of the token. I suggest the destination 
webapp
+     * @param ttlMillis the expiration time
+     * @return a JWT token
+     */
+    public static String createJwt(String id, String issuer, String subject, 
long ttlMillis) {
+        //The JWT signature algorithm we will be using to sign the token
+        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
+
+        long nowMillis = System.currentTimeMillis();
+        Date now = new Date(nowMillis);
+
+        byte[] apiKeySecretBytes = 
DatatypeConverter.parseBase64Binary(ExternalServerJwtMasterSecretKey);
+        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);
+
+        //if it has been specified, let's add the expiration date, this should 
always be true
+        if (ttlMillis >= 0) {
+            long expMillis = nowMillis + ttlMillis;
+            Date exp = new Date(expMillis);
+            builder.setExpiration(exp);
+        }
+
+        //Builds the JWT and serialises it to a compact, URL-safe string
+        return builder.compact();
+    }
+    
+    /**
+     * Reads and validates a JWT token
+     * Throws a SignatureException if it is not a signed JWS (as expected) or 
has been tampered
+     * @param jwt a JWT token
+     * @param id is an Id, I suggest userLoginId
+     * @param issuer is who/what issued the token. I suggest the server DNS
+     * @param subject is the subject of the token. I suggest the destination 
webapp
+     * @return true if the JWT token corresponds to the one sent and is not 
expired
+     */
+    private static boolean checkJwt(String jwt, String id, String issuer, 
String subject) {
+        //The JWT signature algorithm is using this to sign the token
+        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
+
+        byte[] apiKeySecretBytes = 
DatatypeConverter.parseBase64Binary(ExternalServerJwtMasterSecretKey);
+        Key signingKey = new SecretKeySpec(apiKeySecretBytes, 
signatureAlgorithm.getJcaName());
+
+        //This line will throw a SignatureException if it is not a signed JWS 
(as expected) or has been tampered
+        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);
+    }
+
+    public static String getExternalServerName(HttpServletRequest request) {
+        String reportingServerName = "";
+        Delegator delegator = (Delegator) request.getAttribute("delegator");
+        if (delegator != null && 
"Y".equals(EntityUtilProperties.getPropertyValue("embisphere", 
"use-external-server", "Y", delegator))) {
+            reportingServerName = 
EntityUtilProperties.getPropertyValue("embisphere", "external-server-name", 
"localhost:8443", delegator);
+            String reportingServerQuery = 
EntityUtilProperties.getPropertyValue("embisphere", "external-server-query", 
"/catalog/control/", delegator);
+            reportingServerName = "https://"; + reportingServerName + 
reportingServerQuery;
+        }
+        return reportingServerName;
+    }
+    
+    public static long getJwtTokenTimeToLive(HttpServletRequest request) {
+        Delegator delegator = (Delegator) request.getAttribute("delegator");
+        if (delegator != null) return 1000 * 
Long.parseLong(EntityUtilProperties.getPropertyValue("embisphere", 
"external-server-token-duration", "30", delegator));
+        else return 1000 * 30;
+    }
 
 }


Reply via email to