From e34f26a091308a61c101a464c1d66a4d8689a950 Mon Sep 17 00:00:00 2001 From: Jens Hardings Date: Tue, 4 Jun 2024 10:18:58 -0400 Subject: [PATCH 1/2] Add SSO login using access token --- .../service/org/moqui/impl/UserServices.xml | 2 +- .../context/ExecutionContextFactoryImpl.groovy | 1 + .../impl/context/ResourceFacadeImpl.groovy | 12 ++++++++++++ .../moqui/impl/context/UserFacadeImpl.groovy | 18 ++++++++++++++++++ .../java/org/moqui/context/UserFacade.java | 6 ++++++ .../SingleSignOnTokenLoginHandler.java | 7 +++++++ framework/xsd/moqui-conf-3.xsd | 1 + 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java diff --git a/framework/service/org/moqui/impl/UserServices.xml b/framework/service/org/moqui/impl/UserServices.xml index a82c80322..2cb01c913 100644 --- a/framework/service/org/moqui/impl/UserServices.xml +++ b/framework/service/org/moqui/impl/UserServices.xml @@ -96,7 +96,7 @@ along with this software (see the LICENSE.md file). If not, see - + diff --git a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy index c1804561d..f531f9216 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ExecutionContextFactoryImpl.groovy @@ -1642,6 +1642,7 @@ class ExecutionContextFactoryImpl implements ExecutionContextFactory { if (overrideNode.hasChild("user-facade")) { MNode ufBaseNode = baseNode.first("user-facade") MNode ufOverrideNode = overrideNode.first("user-facade") + ufBaseNode.attributes.putAll(ufOverrideNode.attributes) ufBaseNode.mergeSingleChild(ufOverrideNode, "password") ufBaseNode.mergeSingleChild(ufOverrideNode, "login-key") ufBaseNode.mergeSingleChild(ufOverrideNode, "login") diff --git a/framework/src/main/groovy/org/moqui/impl/context/ResourceFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/ResourceFacadeImpl.groovy index e45937d9b..840600012 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ResourceFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/ResourceFacadeImpl.groovy @@ -24,6 +24,7 @@ import org.moqui.impl.context.runner.JavaxScriptRunner import org.moqui.impl.context.runner.XmlActionsScriptRunner import org.moqui.impl.entity.EntityValueBase import org.moqui.jcache.MCache +import org.moqui.security.SingleSignOnTokenLoginHandler import org.moqui.util.ContextBinding import org.moqui.util.ContextStack import org.moqui.util.MNode @@ -59,6 +60,7 @@ class ResourceFacadeImpl implements ResourceFacade { final FtlTemplateRenderer ftlTemplateRenderer final XmlActionsScriptRunner xmlActionsScriptRunner + protected final ToolFactory ssoTokenHandlerFactory = null // the groovy Script object is not thread safe, so have one per thread per expression; can be reused as thread is reused protected final ThreadLocal> threadScriptByExpression = new ThreadLocal<>() @@ -164,6 +166,16 @@ class ResourceFacadeImpl implements ResourceFacade { logger.error("Error getting JCR Repository ${repositoryNode.attribute("name")}: ${e.toString()}") } } + + MNode userFacadeNode = ecfi.confXmlRoot.first("user-facade") + if (userFacadeNode.attribute("sso-access-token-handler-factory")) { + ssoTokenHandlerFactory = ecfi.getToolFactory(userFacadeNode.attribute("sso-access-token-handler-factory")) + if (ssoTokenHandlerFactory != null) { + logger.info("Using sso-access-token-handler-factory ${userFacadeNode.attribute("sso-access-token-handler-factory")} (${ssoTokenHandlerFactory.class.name})") + } else { + logger.warn("Could not find sso-access-token-handler-factory with name ${userFacadeNode.attribute("sso-access-token-handler-factory")}") + } + } } void destroyAllInThread() { diff --git a/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy index abd9f7b23..af2aaaae7 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy @@ -17,6 +17,7 @@ import groovy.transform.CompileStatic import org.apache.shiro.authc.AuthenticationToken import org.apache.shiro.authc.ExpiredCredentialsException import org.moqui.context.PasswordChangeRequiredException +import org.moqui.security.SingleSignOnTokenLoginHandler import javax.websocket.server.HandshakeRequest import java.sql.Timestamp @@ -172,6 +173,14 @@ class UserFacadeImpl implements UserFacade { if (loginKey != null && !loginKey.isEmpty() && !"null".equals(loginKey) && !"undefined".equals(loginKey)) this.loginUserKey(loginKey) } + if (currentInfo.username == null && request.getHeader("sso_access_token")) { + String ssoAccessToken = request.getHeader("sso_access_token").trim() + String ssoAuthFlowId = request.getHeader("sso_auth_flow") + if (ssoAuthFlowId) + ssoAuthFlowId = ssoAuthFlowId.trim() + if (!ssoAccessToken.isEmpty() && !"null".equals(ssoAccessToken) && !"undefined".equals(ssoAccessToken)) + this.loginSsoToken(ssoAccessToken, ssoAuthFlowId) + } if (currentInfo.username == null && secureParameters.authUsername) { // try the Moqui-specific parameters for instant login // if we have credentials coming in anywhere other than URL parameters, try logging in @@ -802,6 +811,15 @@ class UserFacadeImpl implements UserFacade { return loginKey } + @Override boolean loginSsoToken(String ssoAccessToken, String ssoAuthFlowId) { + if (eci.resourceFacade.ssoTokenHandlerFactory == null) { + eci.logger.error("No SingleSignOnTokenLoginHandler ToolFactory configured, cannot handle SsoToken login") + return false + } + final SingleSignOnTokenLoginHandler ssoTokenLoginHandler = eci.resourceFacade.ssoTokenHandlerFactory.getInstance() + return ssoTokenLoginHandler.handleSsoLoginToken(eci, ssoAccessToken, ssoAuthFlowId) + } + @Override boolean loginAnonymousIfNoUser() { if (currentInfo.username == null && !currentInfo.loggedInAnonymous) { currentInfo.loggedInAnonymous = true diff --git a/framework/src/main/java/org/moqui/context/UserFacade.java b/framework/src/main/java/org/moqui/context/UserFacade.java index 209534a85..0e5529770 100644 --- a/framework/src/main/java/org/moqui/context/UserFacade.java +++ b/framework/src/main/java/org/moqui/context/UserFacade.java @@ -105,6 +105,12 @@ public interface UserFacade { String getLoginKey(); String getLoginKey(float expireHours); + /** Authenticate a user and make active using a SSO access token + * @param ssoAccessToken the accessToken provided by the SSO server + * @param ssoAuthFlowId the (optional) authFlowId for identifying the SSO server + */ + boolean loginSsoToken(String ssoAccessToken, String ssoAuthFlowId); + /** If no user is logged in consider an anonymous user logged in. For internal purposes to run things that require authentication. */ boolean loginAnonymousIfNoUser(); diff --git a/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java b/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java new file mode 100644 index 000000000..fe1f2004b --- /dev/null +++ b/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java @@ -0,0 +1,7 @@ +package org.moqui.security; + +import org.moqui.context.ExecutionContext; + +public interface SingleSignOnTokenLoginHandler { + public boolean handleSsoLoginToken(ExecutionContext ec, String ssoAccessToken, String ssoAuthFlowId); +} diff --git a/framework/xsd/moqui-conf-3.xsd b/framework/xsd/moqui-conf-3.xsd index d060a6176..7d623cf51 100644 --- a/framework/xsd/moqui-conf-3.xsd +++ b/framework/xsd/moqui-conf-3.xsd @@ -335,6 +335,7 @@ along with this software (see the LICENSE.md file). If not, see + From 884467d2b1beab0863e578d6c51eeeea4baee9c4 Mon Sep 17 00:00:00 2001 From: Jens Hardings Date: Wed, 17 Jul 2024 12:46:52 -0400 Subject: [PATCH 2/2] fix: do not depend on WebFacade to be instantiated when processing SsoLoginToken --- .../org/moqui/impl/context/ArtifactExecutionInfoImpl.java | 2 +- .../groovy/org/moqui/impl/context/UserFacadeImpl.groovy | 6 +++--- framework/src/main/java/org/moqui/context/UserFacade.java | 4 +++- .../org/moqui/security/SingleSignOnTokenLoginHandler.java | 5 ++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java index 67fe40d54..fa4f0e680 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java +++ b/framework/src/main/groovy/org/moqui/impl/context/ArtifactExecutionInfoImpl.java @@ -180,7 +180,7 @@ public long getChildrenRunningTime() { @Override public ArtifactExecutionInfo getParent() { return parentAeii; } @Override - public BigDecimal getPercentOfParentTime() { return parentAeii != null && endTimeNanos != 0 ? + public BigDecimal getPercentOfParentTime() { return parentAeii != null && endTimeNanos != 0 && parentAeii.endTimeNanos != 0 ? new BigDecimal((getRunningTime() / parentAeii.getRunningTime()) * 100).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO; } diff --git a/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy index af2aaaae7..d23c7b7e9 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/UserFacadeImpl.groovy @@ -179,7 +179,7 @@ class UserFacadeImpl implements UserFacade { if (ssoAuthFlowId) ssoAuthFlowId = ssoAuthFlowId.trim() if (!ssoAccessToken.isEmpty() && !"null".equals(ssoAccessToken) && !"undefined".equals(ssoAccessToken)) - this.loginSsoToken(ssoAccessToken, ssoAuthFlowId) + this.loginSsoToken(ssoAccessToken, ssoAuthFlowId, request, response) } if (currentInfo.username == null && secureParameters.authUsername) { // try the Moqui-specific parameters for instant login @@ -811,13 +811,13 @@ class UserFacadeImpl implements UserFacade { return loginKey } - @Override boolean loginSsoToken(String ssoAccessToken, String ssoAuthFlowId) { + @Override boolean loginSsoToken(String ssoAccessToken, String ssoAuthFlowId, HttpServletRequest request, HttpServletResponse response) { if (eci.resourceFacade.ssoTokenHandlerFactory == null) { eci.logger.error("No SingleSignOnTokenLoginHandler ToolFactory configured, cannot handle SsoToken login") return false } final SingleSignOnTokenLoginHandler ssoTokenLoginHandler = eci.resourceFacade.ssoTokenHandlerFactory.getInstance() - return ssoTokenLoginHandler.handleSsoLoginToken(eci, ssoAccessToken, ssoAuthFlowId) + return ssoTokenLoginHandler.handleSsoLoginToken(eci, request, response, ssoAccessToken, ssoAuthFlowId) } @Override boolean loginAnonymousIfNoUser() { diff --git a/framework/src/main/java/org/moqui/context/UserFacade.java b/framework/src/main/java/org/moqui/context/UserFacade.java index 0e5529770..a52b11567 100644 --- a/framework/src/main/java/org/moqui/context/UserFacade.java +++ b/framework/src/main/java/org/moqui/context/UserFacade.java @@ -15,6 +15,8 @@ import org.moqui.entity.EntityValue; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.sql.Timestamp; import java.util.*; @@ -109,7 +111,7 @@ public interface UserFacade { * @param ssoAccessToken the accessToken provided by the SSO server * @param ssoAuthFlowId the (optional) authFlowId for identifying the SSO server */ - boolean loginSsoToken(String ssoAccessToken, String ssoAuthFlowId); + boolean loginSsoToken(String ssoAccessToken, String ssoAuthFlowId, HttpServletRequest request, HttpServletResponse response); /** If no user is logged in consider an anonymous user logged in. For internal purposes to run things that require authentication. */ boolean loginAnonymousIfNoUser(); diff --git a/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java b/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java index fe1f2004b..9668942e1 100644 --- a/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java +++ b/framework/src/main/java/org/moqui/security/SingleSignOnTokenLoginHandler.java @@ -2,6 +2,9 @@ import org.moqui.context.ExecutionContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + public interface SingleSignOnTokenLoginHandler { - public boolean handleSsoLoginToken(ExecutionContext ec, String ssoAccessToken, String ssoAuthFlowId); + public boolean handleSsoLoginToken(ExecutionContext ec, HttpServletRequest request, HttpServletResponse response, String ssoAccessToken, String ssoAuthFlowId); }