/*********************************************************************** * 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: * * - Carlos Ruiz (sponsored by FH) * **********************************************************************/ package org.compiere.model; import static org.compiere.model.SystemIDs.OAUTH2_AUTHORIZATION_PROVIDER_MICROSOFT; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.adempiere.exceptions.AdempiereException; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; import org.compiere.util.EMail; import org.compiere.util.Env; import org.compiere.util.Msg; import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest; import com.google.api.client.auth.oauth2.ClientParametersAuthentication; import com.google.api.client.auth.oauth2.TokenResponse; import com.google.api.client.auth.openidconnect.IdToken; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; /** * Authorization Credential */ public class MAuthorizationCredential extends X_AD_AuthorizationCredential { /** * generated serial id */ private static final long serialVersionUID = -5410915257751308490L; /** * UUID based Constructor * @param ctx Context * @param AD_AuthorizationCredential_UU UUID key * @param trxName Transaction */ public MAuthorizationCredential(Properties ctx, String AD_AuthorizationCredential_UU, String trxName) { super(ctx, AD_AuthorizationCredential_UU, trxName); } /** * Create empty Authorization Credential * * @param ctx context * @param AD_AuthorizationCredential_ID ID * @param trxName transaction */ public MAuthorizationCredential(Properties ctx, int AD_AuthorizationCredential_ID, String trxName) { super(ctx, AD_AuthorizationCredential_ID, trxName); } // MAuthorizationCredential /** * Create Authorization Credential from current row in ResultSet * * @param ctx context * @param rs ResultSet * @param trxName transaction */ public MAuthorizationCredential(Properties ctx, ResultSet rs, String trxName) { super(ctx, rs, trxName); } // MAuthorizationCredential /** * Create or Update an Account based on the token received * @param code * @param pinstance * @return error message or null */ public String processToken(String code, MPInstance pinstance) { String msg = null; try { if (pinstance == null) { // this is not expected, just added here for safety msg = "Process instance is required"; return msg; } // get the scope parameter MPInstancePara paramScope = null; for (MPInstancePara param : pinstance.getParameters()) { if ("AD_AuthorizationScopes".equals(param.getParameterName())) { paramScope = param; break; } } if (paramScope == null) { // this is not expected, just added here for safety msg = "Process instance parameter for Scopes not found"; return msg; } MPInstanceLog pilog = pinstance.addLog(null, 0, null, null, MAuthorizationAccount.Table_ID, 0); msg = processToken(Env.getCtx(), code, paramScope.getP_String(), pilog); pilog.saveEx(); } catch (Exception ex) { ex.printStackTrace(); msg = Msg.getMsg(getCtx(), "Error") + ex.getLocalizedMessage(); return msg; } return msg; } /** * Create or Update an Account based on the token received * @param ctx * @param code * @param paramScope * @param pilog MPInstanceLog to set the log message and record_ID, it is not saved, the caller must save it * @return error message or null */ public String processToken(Properties ctx, String code, String paramScope, MPInstanceLog pilog) { String msg = null; try { String clientId = getAuthorizationClientId(); String clientSecret = getAuthorizationClientSecret(); Timestamp ts = new Timestamp(System.currentTimeMillis()); MAuthorizationProvider ap = new MAuthorizationProvider(ctx, getAD_AuthorizationProvider_ID(), get_TrxName()); AuthorizationCodeTokenRequest request = new AuthorizationCodeTokenRequest(new NetHttpTransport(), GsonFactory.getDefaultInstance(), new GenericUrl(ap.getTokenEndpoint()), code); request.setRedirectUri(getAuthorizationRedirectURL()); request.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)); TokenResponse tokenResponse = request.execute(); Object id_token = tokenResponse.get("id_token"); String email = null; if (id_token != null && id_token instanceof String) { IdToken idtoken = IdToken.parse(tokenResponse.getFactory(), (String) tokenResponse.get("id_token")); email = (String) idtoken.getPayload().get("email"); } if (email == null) { msg = Msg.parseTranslation(ctx, "@Error@ @OAuthProcessToken_CouldNotGetEMail@"); return msg; } String preferred_username = null; if ( ap.getAD_AuthorizationProvider_ID() == OAUTH2_AUTHORIZATION_PROVIDER_MICROSOFT && MSysConfig.getBooleanValue(MSysConfig.OAUTH2_USE_ID_TOKEN_PREF_USERNAME_ON_MS_PROVIDER, true)) { /* Microsoft send the user email information in the id_token in preferred_username field in some cases */ if (id_token != null && id_token instanceof String) { IdToken idtoken = IdToken.parse(tokenResponse.getFactory(), (String) tokenResponse.get("id_token")); preferred_username = (String) idtoken.getPayload().get("preferred_username"); } } if ( preferred_username == null && ap.getAD_AuthorizationProvider_ID() == OAUTH2_AUTHORIZATION_PROVIDER_MICROSOFT && MSysConfig.getBooleanValue(MSysConfig.OAUTH2_USE_ACCESS_TOKEN_UPN_ON_MICROSOFT_PROVIDER, true)) { /* Microsoft send the user email information in the access_token in upn field in some cases */ Object access_token = tokenResponse.get("access_token"); if (access_token != null && access_token instanceof String) { try { IdToken accesstoken = IdToken.parse(tokenResponse.getFactory(), (String) tokenResponse.get("access_token")); preferred_username = (String) accesstoken.getPayload().get("upn"); } catch (Exception ex) { // accesstoken not valid ... simply ignore } } } boolean newAccount = false; MAuthorizationAccount account = null; Query query = new Query(ctx, MAuthorizationAccount.Table_Name, "AD_Client_ID=? AND AD_User_ID=? AND EMail=? AND AD_AuthorizationCredential_ID=?", get_TrxName()); query.setParameters(Env.getAD_Client_ID(ctx), Env.getAD_User_ID(ctx), email, getAD_AuthorizationCredential_ID()); account = query.setOnlyActiveRecords(true).first(); if (account == null) { account = new MAuthorizationAccount(ctx, 0, get_TrxName()); account.setEMail(email); if (preferred_username != null && ! email.toLowerCase().equals(preferred_username.toLowerCase()) && EMail.validate(preferred_username)) { account.setPreferred_UserName(preferred_username); } account.setAD_AuthorizationCredential_ID(getAD_AuthorizationCredential_ID()); account.setAD_User_ID(Env.getAD_User_ID(ctx)); newAccount = true; } account.setAD_AuthorizationScopes(paramScope); account.setAccessToken(tokenResponse.getAccessToken()); account.setAccessTokenTimestamp(ts); account.setExpireInSeconds(BigDecimal.valueOf(tokenResponse.getExpiresInSeconds())); account.setIsAuthorized(true); account.setIsActive(true); if (tokenResponse.getRefreshToken() == null && account.getRefreshToken() == null) { String refreshToken = account.findRefreshToken(); if (refreshToken != null) { account.setRefreshToken(refreshToken); } } if (tokenResponse.getRefreshToken() == null && account.getRefreshToken() == null) { //revoke access and ask for retry MAuthorizationProvider provider = new MAuthorizationProvider(ctx, getAD_AuthorizationProvider_ID(), get_TrxName()); String revokeEndPoint = provider.getRevokeEndpoint(); if (revokeEndPoint != null) { HttpRequestFactory factory = new NetHttpTransport().createRequestFactory(); GenericUrl url = new GenericUrl(revokeEndPoint + "?token="+account.getAccessToken()); HttpRequest revokeRequest = factory.buildGetRequest(url); revokeRequest.execute(); } msg = Msg.parseTranslation(ctx, "@Error@ @OAuthProcessToken_NoRefreshToken@"); return msg; } if (tokenResponse.getRefreshToken() != null) { account.setRefreshToken(tokenResponse.getRefreshToken()); } account.saveEx(); if (pilog != null) { String logmsg = Msg.parseTranslation(ctx, (newAccount ? "@Created@" : "@Updated@") + " @AD_AuthorizationAccount_ID@ for ") + account.getEMail(); pilog.setP_Msg(logmsg); pilog.setRecord_ID(account.getAD_AuthorizationAccount_ID()); } account.syncOthers(); if (newAccount) msg = Msg.getMsg(ctx, "Authorization_Access_OK", new Object[] {account.getEMail(), paramScope}); else msg = Msg.getMsg(ctx, "Authorization_Access_Previous", new Object[] {account.getEMail(), paramScope}); } catch (Exception ex) { ex.printStackTrace(); msg = Msg.getMsg(ctx, "Error") + ex.getLocalizedMessage(); return msg; } return msg; } /** * Get a complete Authorization end point URL with all the parameters required * @param scopes * @param state * @return authorization url */ public String getFullAuthorizationEndpoint(String scopes, String state) { String scopeUrl = findScopeUrl(scopes); if (scopeUrl == null) throw new AdempiereException("Could not find scope " + scopes + " for provider " + getAD_AuthorizationProvider_ID()); MAuthorizationProvider provider = new MAuthorizationProvider(getCtx(), getAD_AuthorizationProvider_ID(), get_TrxName()); String authEndPoint = provider.getAuthorizationEndpoint(); StringBuilder url = new StringBuilder(authEndPoint).append("?"); List nameValuePairs = new ArrayList<>(); nameValuePairs.add(new BasicNameValuePair("scope", scopeUrl)); nameValuePairs.add(new BasicNameValuePair("redirect_uri", getAuthorizationRedirectURL())); nameValuePairs.add(new BasicNameValuePair("access_type", "offline")); // required by google nameValuePairs.add(new BasicNameValuePair("response_type", "code")); nameValuePairs.add(new BasicNameValuePair("client_id", getAuthorizationClientId())); nameValuePairs.add(new BasicNameValuePair("state", state)); url.append(URLEncodedUtils.format(nameValuePairs, "UTF-8")); return url.toString(); } /** * Get the scope URL for the authorization provider * @param scopes * @return scope urls (separated by space) */ private String findScopeUrl(String scopes) { StringBuilder urlBuilder = new StringBuilder(); List scopeURLs = new ArrayList(); String[] scopeList = scopes.split("[,]"); for(String scope : scopeList) { scope = scope.trim(); MAuthorizationScopeProv scpr = new Query(Env.getCtx(), MAuthorizationScopeProv.Table_Name, "AD_AuthorizationProvider_ID=? AND AD_AuthorizationScope=?", get_TrxName()) .setOnlyActiveRecords(true) .setParameters(getAD_AuthorizationProvider_ID(), scope) .first(); if (scpr != null) { String[] urls = scpr.getScopeURL().split("\\s+"); for(String url : urls) { url = url.trim(); if (!scopeURLs.contains(url)) scopeURLs.add(url); } } } for(String scopeURL : scopeURLs) { if (urlBuilder.length() > 0) urlBuilder.append(" "); urlBuilder.append(scopeURL); } return urlBuilder.length() > 0 ? urlBuilder.toString() : null; } /** * Get Authorization Redirect URL parsing context variables * @return Authorization Redirect URL */ @Override public String getAuthorizationRedirectURL() { return Env.parseContext(getCtx(), 0, super.getAuthorizationRedirectURL(), false, false); } } // MAuthorizationCredential