From 3ffdd2be0c4f885772fa2a840eaed856e797b6b2 Mon Sep 17 00:00:00 2001 From: hengsin Date: Wed, 4 Oct 2023 02:26:03 +0800 Subject: [PATCH] IDEMPIERE-5346 SSO Support (#2038) * IDEMPIERE-5346 SSO Support - add OIDC support to core --- .../oracle/202310021327_IDEMPIERE-5346.sql | 10 + .../202310021327_IDEMPIERE-5346.sql | 7 + org.adempiere.server-feature/feature.xml | 6 + .../server.product.launch | 1 + .../webui/apps/form/WResetPassword.java | 1 - org.idempiere.ui.sso.oidc/.classpath | 17 + org.idempiere.ui.sso.oidc/.project | 33 ++ .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.pde.core.prefs | 3 + .../META-INF/MANIFEST.MF | 27 ++ ...ui.sso.oidc.factory.OIDCServiceFactory.xml | 8 + org.idempiere.ui.sso.oidc/README.md | 3 + org.idempiere.ui.sso.oidc/build.properties | 11 + org.idempiere.ui.sso.oidc/pom.xml | 90 ++++++ .../sso/oidc/factory/OIDCServiceFactory.java | 58 ++++ .../oidc/service/OIDCPrincipalService.java | 305 ++++++++++++++++++ pom.xml | 1 + 17 files changed, 582 insertions(+), 1 deletion(-) create mode 100644 migration/iD11/oracle/202310021327_IDEMPIERE-5346.sql create mode 100644 migration/iD11/postgresql/202310021327_IDEMPIERE-5346.sql create mode 100644 org.idempiere.ui.sso.oidc/.classpath create mode 100644 org.idempiere.ui.sso.oidc/.project create mode 100644 org.idempiere.ui.sso.oidc/.settings/org.eclipse.core.resources.prefs create mode 100644 org.idempiere.ui.sso.oidc/.settings/org.eclipse.pde.core.prefs create mode 100644 org.idempiere.ui.sso.oidc/META-INF/MANIFEST.MF create mode 100644 org.idempiere.ui.sso.oidc/OSGI-INF/org.idempiere.ui.sso.oidc.factory.OIDCServiceFactory.xml create mode 100644 org.idempiere.ui.sso.oidc/README.md create mode 100644 org.idempiere.ui.sso.oidc/build.properties create mode 100644 org.idempiere.ui.sso.oidc/pom.xml create mode 100644 org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/factory/OIDCServiceFactory.java create mode 100644 org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/service/OIDCPrincipalService.java diff --git a/migration/iD11/oracle/202310021327_IDEMPIERE-5346.sql b/migration/iD11/oracle/202310021327_IDEMPIERE-5346.sql new file mode 100644 index 0000000000..4715fd9ed5 --- /dev/null +++ b/migration/iD11/oracle/202310021327_IDEMPIERE-5346.sql @@ -0,0 +1,10 @@ +-- IDEMPIERE-5346 SSO Support +SELECT register_migration_script('202310021327_IDEMPIERE-5346.sql') FROM dual; + +SET SQLBLANKLINES ON +SET DEFINE OFF + +-- Oct 2, 2023, 1:27:01 PM MYT +INSERT INTO AD_Ref_List (AD_Ref_List_ID,Name,AD_Reference_ID,Value,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,EntityType,AD_Ref_List_UU) VALUES (200656,'OpenID Connect',200213,'OIDC',0,0,'Y',TO_TIMESTAMP('2023-10-02 13:27:00','YYYY-MM-DD HH24:MI:SS'),100,TO_TIMESTAMP('2023-10-02 13:27:00','YYYY-MM-DD HH24:MI:SS'),100,'D','e1913c90-dcac-4a0d-bd55-dfa3408654e8') +; + diff --git a/migration/iD11/postgresql/202310021327_IDEMPIERE-5346.sql b/migration/iD11/postgresql/202310021327_IDEMPIERE-5346.sql new file mode 100644 index 0000000000..16ea241ce3 --- /dev/null +++ b/migration/iD11/postgresql/202310021327_IDEMPIERE-5346.sql @@ -0,0 +1,7 @@ +-- IDEMPIERE-5346 SSO Support +SELECT register_migration_script('202310021327_IDEMPIERE-5346.sql') FROM dual; + +-- Oct 2, 2023, 1:27:01 PM MYT +INSERT INTO AD_Ref_List (AD_Ref_List_ID,Name,AD_Reference_ID,Value,AD_Client_ID,AD_Org_ID,IsActive,Created,CreatedBy,Updated,UpdatedBy,EntityType,AD_Ref_List_UU) VALUES (200656,'OpenID Connect',200213,'OIDC',0,0,'Y',TO_TIMESTAMP('2023-10-02 13:27:00','YYYY-MM-DD HH24:MI:SS'),100,TO_TIMESTAMP('2023-10-02 13:27:00','YYYY-MM-DD HH24:MI:SS'),100,'D','e1913c90-dcac-4a0d-bd55-dfa3408654e8') +; + diff --git a/org.adempiere.server-feature/feature.xml b/org.adempiere.server-feature/feature.xml index ffe74c08c4..c7a369b6cc 100644 --- a/org.adempiere.server-feature/feature.xml +++ b/org.adempiere.server-feature/feature.xml @@ -687,4 +687,10 @@ fragment="true" unpack="false"/> + diff --git a/org.adempiere.server-feature/server.product.launch b/org.adempiere.server-feature/server.product.launch index c7a5073e90..e64e4fa5fa 100644 --- a/org.adempiere.server-feature/server.product.launch +++ b/org.adempiere.server-feature/server.product.launch @@ -440,6 +440,7 @@ + diff --git a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/form/WResetPassword.java b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/form/WResetPassword.java index 04befae6d3..fccc26aabf 100644 --- a/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/form/WResetPassword.java +++ b/org.adempiere.ui.zk/WEB-INF/src/org/adempiere/webui/apps/form/WResetPassword.java @@ -42,7 +42,6 @@ import org.compiere.model.MPasswordHistory; import org.compiere.model.MPasswordRule; import org.compiere.model.MSysConfig; import org.compiere.model.MUser; -import org.compiere.model.PO; import org.compiere.model.SystemIDs; import org.compiere.util.CLogger; import org.compiere.util.DisplayType; diff --git a/org.idempiere.ui.sso.oidc/.classpath b/org.idempiere.ui.sso.oidc/.classpath new file mode 100644 index 0000000000..339a695181 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/.classpath @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/org.idempiere.ui.sso.oidc/.project b/org.idempiere.ui.sso.oidc/.project new file mode 100644 index 0000000000..8b4153ca1b --- /dev/null +++ b/org.idempiere.ui.sso.oidc/.project @@ -0,0 +1,33 @@ + + + org.idempiere.ui.sso.oidc + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.idempiere.ui.sso.oidc/.settings/org.eclipse.core.resources.prefs b/org.idempiere.ui.sso.oidc/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/org.idempiere.ui.sso.oidc/.settings/org.eclipse.pde.core.prefs b/org.idempiere.ui.sso.oidc/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 0000000000..f29e940a00 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/org.idempiere.ui.sso.oidc/META-INF/MANIFEST.MF b/org.idempiere.ui.sso.oidc/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..557f965ffa --- /dev/null +++ b/org.idempiere.ui.sso.oidc/META-INF/MANIFEST.MF @@ -0,0 +1,27 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: OIDC Provider +Bundle-SymbolicName: org.idempiere.ui.sso.oidc +Bundle-Version: 11.0.0.qualifier +Bundle-Vendor: iDempiere.org +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: org.idempiere.ui.sso.oidc +Import-Package: javax.servlet;version="4.0.0", + javax.servlet.http;version="4.0.0", + org.osgi.framework;version="1.3.0", + org.osgi.service.component.annotations;version="1.3.0", + org.slf4j;version="1.7.30", + org.slf4j.event;version="1.7.30", + org.slf4j.helpers;version="1.7.30", + org.slf4j.spi;version="1.7.30" +Bundle-ActivationPolicy: lazy +Require-Bundle: org.adempiere.base;bundle-version="11.0.0", + org.eclipse.core.runtime;bundle-version="3.24.100" +Service-Component: OSGI-INF/org.idempiere.ui.sso.oidc.factory.OIDCServiceFactory.xml +Bundle-ClassPath: lib/nimbus-jose-jwt.jar, + lib/oauth2-oidc-sdk.jar, + lib/json-smart.jar, + lib/lang-tag.jar, + lib/accessors-smart.jar, + lib/content-type.jar, + . diff --git a/org.idempiere.ui.sso.oidc/OSGI-INF/org.idempiere.ui.sso.oidc.factory.OIDCServiceFactory.xml b/org.idempiere.ui.sso.oidc/OSGI-INF/org.idempiere.ui.sso.oidc.factory.OIDCServiceFactory.xml new file mode 100644 index 0000000000..302fcd325b --- /dev/null +++ b/org.idempiere.ui.sso.oidc/OSGI-INF/org.idempiere.ui.sso.oidc.factory.OIDCServiceFactory.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/org.idempiere.ui.sso.oidc/README.md b/org.idempiere.ui.sso.oidc/README.md new file mode 100644 index 0000000000..7996860813 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/README.md @@ -0,0 +1,3 @@ +# org.idempiere.ui.sso.oidc + +OpenID Connect SSO provider for iDempiere Web Client diff --git a/org.idempiere.ui.sso.oidc/build.properties b/org.idempiere.ui.sso.oidc/build.properties new file mode 100644 index 0000000000..81f7c3e769 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/build.properties @@ -0,0 +1,11 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + lib/nimbus-jose-jwt.jar,\ + lib/oauth2-oidc-sdk.jar,\ + lib/json-smart.jar,\ + lib/lang-tag.jar,\ + lib/accessors-smart.jar,\ + lib/content-type.jar diff --git a/org.idempiere.ui.sso.oidc/pom.xml b/org.idempiere.ui.sso.oidc/pom.xml new file mode 100644 index 0000000000..16c669cb48 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/pom.xml @@ -0,0 +1,90 @@ + + 4.0.0 + + org.idempiere + org.idempiere.parent + ${revision} + ../org.idempiere.parent/pom.xml + + org.idempiere.ui.sso.oidc + eclipse-plugin + + + + + maven-clean-plugin + + + auto-clean + validate + + clean + + + + + + + ${project.basedir}/lib + + *.jar + + false + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + validate + + copy + + + + + com.nimbusds + oauth2-oidc-sdk + 10.7.1 + + + com.nimbusds + nimbus-jose-jwt + 9.31 + + + net.minidev + json-smart + 2.4.10 + + + com.nimbusds + lang-tag + 1.7 + + + net.minidev + accessors-smart + 2.4.9 + + + com.nimbusds + content-type + 2.2 + + + lib + true + true + true + + + + + + + diff --git a/org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/factory/OIDCServiceFactory.java b/org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/factory/OIDCServiceFactory.java new file mode 100644 index 0000000000..b96efd378a --- /dev/null +++ b/org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/factory/OIDCServiceFactory.java @@ -0,0 +1,58 @@ +/*********************************************************************** + * This file is part of iDempiere ERP Open Source * + * http://www.idempiere.org * + * * + * Copyright (C) Contributors * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301, USA. * + * * + * Contributors: * + * - hengsin * + **********************************************************************/ +package org.idempiere.ui.sso.oidc.factory; + +import org.adempiere.base.sso.ISSOPrincipalFactory; +import org.adempiere.base.sso.ISSOPrincipalService; +import org.compiere.model.I_SSO_PrincipalConfig; +import org.osgi.service.component.annotations.Component; + +import org.idempiere.ui.sso.oidc.service.OIDCPrincipalService; + +/** + * Factory for OIDC principal service + * @author hengsin + */ +@Component(immediate = true, service = org.adempiere.base.sso.ISSOPrincipalFactory.class, property = {"service.ranking:Integer=0"}) +public class OIDCServiceFactory implements ISSOPrincipalFactory { + + /** SSO provider id for OIDC */ + public static final String OIDC_PROVIDER_ID = "OIDC"; + + /** + * Default constructor + */ + public OIDCServiceFactory() { + } + + @Override + public ISSOPrincipalService getSSOPrincipalService(I_SSO_PrincipalConfig config) { + if (config.getSSO_Provider().equals(OIDC_PROVIDER_ID)) + return new OIDCPrincipalService(config); + + return null; + } + +} diff --git a/org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/service/OIDCPrincipalService.java b/org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/service/OIDCPrincipalService.java new file mode 100644 index 0000000000..51f29df7b9 --- /dev/null +++ b/org.idempiere.ui.sso.oidc/src/org/idempiere/ui/sso/oidc/service/OIDCPrincipalService.java @@ -0,0 +1,305 @@ +/*********************************************************************** + * This file is part of iDempiere ERP Open Source * + * http://www.idempiere.org * + * * + * Copyright (C) Contributors * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * + * MA 02110-1301, USA. * + * * + * Contributors: * + * - hengsin * + **********************************************************************/ +package org.idempiere.ui.sso.oidc.service; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.adempiere.base.sso.ISSOPrincipalService; +import org.adempiere.base.sso.SSOUtils; +import org.compiere.model.I_SSO_PrincipalConfig; +import org.compiere.model.MSysConfig; +import org.compiere.util.Language; +import org.compiere.util.Util; + +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.GeneralException; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationRequest; +import com.nimbusds.openid.connect.sdk.AuthenticationResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; +import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser; +import com.nimbusds.openid.connect.sdk.Prompt; +import com.nimbusds.openid.connect.sdk.UserInfoRequest; +import com.nimbusds.openid.connect.sdk.UserInfoResponse; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; + +/** + * Implement SSO principal provider service for OIDC + * @author hengsin + */ +public class OIDCPrincipalService implements ISSOPrincipalService { + + /** code parameter from OIDC */ + private static final String AUTHENTICATION_CODE_PARAMETER = "code"; + + private static final String OIDC_STATE = "oidc.state"; + + private I_SSO_PrincipalConfig principalConfig; + + private OIDCProviderMetadata metaData = null; + + /** + * Default constructor + * @param principleConfig + */ + public OIDCPrincipalService(I_SSO_PrincipalConfig principleConfig) { + this.principalConfig = principleConfig; + } + + @Override + public String getUserName(Object principalObject) throws ParseException { + if (principalObject != null && principalObject instanceof UserInfo) + { + boolean isEmailLogin = MSysConfig.getBooleanValue(MSysConfig.USE_EMAIL_FOR_LOGIN, false); + UserInfo userInfo = (UserInfo) principalObject; + if (isEmailLogin) + return (String) userInfo.getEmailAddress(); + else + return (String) userInfo.getName(); + } + return null; + } + + @Override + public Language getLanguage(Object principalObject) throws ParseException { + return Language.getBaseLanguage(); + } + + @Override + public boolean hasAuthenticationCode(HttpServletRequest request, HttpServletResponse response) { + String code = request.getParameter(AUTHENTICATION_CODE_PARAMETER); + return !Util.isEmpty(code, true); + } + + @Override + public void getAuthenticationToken(HttpServletRequest request, HttpServletResponse response, String redirectMode) + throws Throwable { + String url = request.getRequestURL().toString(); + String query = request.getQueryString(); + if (query != null) { + url += "?" + query; + } + AuthenticationResponse authResponse = AuthenticationResponseParser.parse(new URI(url)); + + // Check the returned state parameter, must match the original + State state = (State) request.getSession().getAttribute(OIDC_STATE); + if (!state.equals(authResponse.getState())) { + // Unexpected or tampered response, stop!!! + request.getSession().removeAttribute(OIDC_STATE); + response.sendRedirect(getRedirectURL(principalConfig, redirectMode)); + return; + } + + if (!authResponse.indicatesSuccess()) { + // The request was denied or some error occurred + AuthenticationErrorResponse errorResponse = authResponse.toErrorResponse(); + throw new RuntimeException(errorResponse.toString()); + } + + AuthenticationSuccessResponse successResponse = authResponse.toSuccessResponse(); + + // Retrieve the authorization code, to be used later to exchange the code for + // an access token at the token endpoint of the server + AuthorizationCode authorizationCode = successResponse.getAuthorizationCode(); + URI redirectURI = new URI(getRedirectURL(principalConfig, redirectMode)); + AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, redirectURI); + + TokenResponse tokenResponse = issueTokenRequest(authorizationGrant); + + processTokenResponse(request, tokenResponse, true); + } + + /** + * @param principalConfig + * @param clientType SSO_MODE_WEBUI, SSO_MODE_MONITOR or SSO_MODE_OSGI (webui, server monitor, osgi console) + * @return redirect url for clientType + */ + private String getRedirectURL(I_SSO_PrincipalConfig principalConfig, String clientType) { + if (SSOUtils.SSO_MODE_WEBUI.equals(clientType)) + return principalConfig.getSSO_ApplicationRedirectURIs(); + else if (SSOUtils.SSO_MODE_MONITOR.equals(clientType)) + return principalConfig.getSSO_IDempMonitorRedirectURIs(); + else if (SSOUtils.SSO_MODE_OSGI.equals(clientType)) + return principalConfig.getSSO_OSGIRedirectURIs(); + else + return null; + } + + /** + * Process token response from keycloak server + * @param request + * @param tokenResponse + * @param getUserInfo true to get UserInfo from userinfo endpoint + * @throws URISyntaxException + * @throws IOException + * @throws GeneralException + */ + private void processTokenResponse(HttpServletRequest request, TokenResponse tokenResponse, boolean getUserInfo) + throws URISyntaxException, IOException, GeneralException { + if (!tokenResponse.indicatesSuccess()) { + // We got an error response... + TokenErrorResponse errorResponse = tokenResponse.toErrorResponse(); + throw new RuntimeException(errorResponse.toJSONObject().toJSONString()); + } + + OIDCTokenResponse oidcTokenResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse(); + + // Get the access token + AccessToken accessToken = oidcTokenResponse.getOIDCTokens().getAccessToken(); + + if (getUserInfo) { + String userInfoURL = getMetaData().getUserInfoEndpointURI().toString(); + URI userInfoEndpoint = new URI(userInfoURL); + BearerAccessToken bearerAccessToken = new BearerAccessToken(accessToken.getValue()); // The access token + + // Make the request + HTTPResponse httpResponse = new UserInfoRequest(userInfoEndpoint, bearerAccessToken) + .toHTTPRequest() + .send(); + + // Parse the response + UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse); + + if (!userInfoResponse.indicatesSuccess()) { + // The request failed, e.g. due to invalid or expired token + throw new RuntimeException(userInfoResponse.toErrorResponse().toString()); + } + + // Extract the claims + UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo(); + setAuthenticationResult(request.getSession(), userInfo); + } + } + + private OIDCProviderMetadata getMetaData() throws GeneralException, IOException { + if (metaData == null) { + String discoveryURI = principalConfig.getSSO_ApplicationDiscoveryURI(); + Issuer issuer = new Issuer(discoveryURI.substring(0, discoveryURI.indexOf("/.well-known/openid-configuration"))); + metaData = OIDCProviderMetadata.resolve(issuer); + } + return metaData; + } + + /** + * Send Token request to oidc token endpoint + * @param authorizationGrant + * @return TokenResponse + * @throws URISyntaxException + * @throws IOException + * @throws GeneralException + */ + private TokenResponse issueTokenRequest(AuthorizationGrant authorizationGrant) throws URISyntaxException, + IOException, GeneralException { + // The credentials to authenticate the client at the token endpoint + ClientID clientID = new ClientID(principalConfig.getSSO_ApplicationClientID()); + Secret clientSecret = new Secret(principalConfig.getSSO_ApplicationSecretKey()); + ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret); + + // The token endpoint + String tokenEndpointURL = getMetaData().getTokenEndpointURI().toString(); + URI tokenEndpoint = new URI(tokenEndpointURL); + + // Make the token request + TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuth, authorizationGrant); + + return OIDCTokenResponseParser.parse(tokenRequest.toHTTPRequest().send()); + } + + /** + * Store UserInfo as session token + * @param session + * @param userInfo + */ + private void setAuthenticationResult(HttpSession session, UserInfo userInfo) { + session.setAttribute(ISSOPrincipalService.SSO_PRINCIPAL_SESSION_TOKEN, userInfo); + } + + @Override + public boolean isAuthenticated(HttpServletRequest request, HttpServletResponse response) { + return request.getSession().getAttribute(ISSOPrincipalService.SSO_PRINCIPAL_SESSION_TOKEN) != null; + } + + @Override + public void redirectForAuthentication(HttpServletRequest request, HttpServletResponse response, String redirectMode) + throws IOException { + AuthenticationRequest authRequest = null; + try { + String url = getMetaData().getAuthorizationEndpointURI().toString(); + authRequest = new AuthenticationRequest.Builder( + new ResponseType(AUTHENTICATION_CODE_PARAMETER), + new Scope("openid", "profile", "email"), + new ClientID(principalConfig.getSSO_ApplicationClientID()), + new URI(getRedirectURL(principalConfig, redirectMode))) + .state(new State()) + .nonce(new Nonce()) + .prompt(new Prompt(Prompt.Type.LOGIN)) + .endpointURI(new URI(url)) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } catch (GeneralException e) { + throw new RuntimeException(e); + } + + //store state for response verification + request.getSession().setAttribute(OIDC_STATE, authRequest.getState()); + URI redirectURI = authRequest.toURI(); + + response.sendRedirect(redirectURI.toURL().toString()); + } + + @Override + public void removePrincipalFromSession(HttpServletRequest httpRequest) { + httpRequest.getSession().removeAttribute(ISSOPrincipalService.SSO_PRINCIPAL_SESSION_TOKEN); + httpRequest.getSession().removeAttribute(OIDC_STATE); + } +} diff --git a/pom.xml b/pom.xml index f955a32e3d..9dcbcf11dd 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ org.idempiere.zk-feature org.idempiere.webservices.client-feature org.idempiere.jetty.osgi.boot.fragment + org.idempiere.ui.sso.oidc org.idempiere.p2 org.idempiere.javadoc org.idempiere.test-feature