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; + } } |
Free forum by Nabble | Edit this page |