diff --git a/rundl.sh b/rundl.sh index 98c917a7..e21e59a7 100755 --- a/rundl.sh +++ b/rundl.sh @@ -1,4 +1,4 @@ -#!/bin/zsh +#!/bin/bash # Reset getopts state (important for zsh) OPTIND=1 @@ -9,9 +9,11 @@ batchmodeargs="" encryptionargs="" configdir="salesforce.config.dir=./configs" version="" +keyfile="" +password="" # Parse command line options -while getopts ":dbe:v:" flag; do +while getopts ":dbe:v:k:D:" flag; do case "${flag}" in d) debug="-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=0.0.0.0:5005,suspend=y" @@ -23,7 +25,28 @@ while getopts ":dbe:v:" flag; do configdir="" ;; e) - encryptionargs="run.mode=encrypt" + encryptionargs="run.mode=encrypt -e " + password="${OPTARG}" + # Check if next argument exists and is not another option + if [[ $OPTIND -le $# && ${!OPTIND} != -* ]]; then + keyfile="${!OPTIND}" + ((OPTIND++)) + fi + configdir="" + ;; + D) + encryptionargs="run.mode=encrypt -d " + password="${OPTARG}" + # Check if next argument exists and is not another option + if [[ $OPTIND -le $# && ${!OPTIND} != -* ]]; then + keyfile="${!OPTIND}" + ((OPTIND++)) + fi + configdir="" + ;; + k) + encryptionargs="run.mode=encrypt -k " + keyfile="${OPTARG}" configdir="" ;; v) @@ -67,4 +90,4 @@ if [[ -n "$debug" ]]; then fi # Execute Data Loader -exec "${DATALOADER_JAVA_HOME}/bin/java" --enable-native-access=ALL-UNNAMED ${=debug} -cp "${jarname}" com.salesforce.dataloader.process.DataLoaderRunner ${=batchmodeargs} ${=configdir} ${=encryptionargs} "$@" +${DATALOADER_JAVA_HOME}/bin/java --enable-native-access=ALL-UNNAMED ${debug} -cp "${jarname}" com.salesforce.dataloader.process.DataLoaderRunner ${batchmodeargs} ${configdir} ${encryptionargs} ${password} ${keyfile} $@ diff --git a/src/main/java/com/salesforce/dataloader/client/LoginClient.java b/src/main/java/com/salesforce/dataloader/client/LoginClient.java index 0f031571..60161290 100644 --- a/src/main/java/com/salesforce/dataloader/client/LoginClient.java +++ b/src/main/java/com/salesforce/dataloader/client/LoginClient.java @@ -326,6 +326,9 @@ public static String getServicePath() { public LimitInfo getAPILimitInfo() { LimitInfoHeader_element limitInfoElement = getConnection().getLimitInfoHeader(); + if (limitInfoElement == null || limitInfoElement.getLimitInfo() == null) { + return null; + } for (LimitInfo info : limitInfoElement.getLimitInfo()) { if ("API REQUESTS".equalsIgnoreCase(info.getType())) { return info; diff --git a/src/main/java/com/salesforce/dataloader/config/AppConfig.java b/src/main/java/com/salesforce/dataloader/config/AppConfig.java index bb4cab91..d00e9db0 100644 --- a/src/main/java/com/salesforce/dataloader/config/AppConfig.java +++ b/src/main/java/com/salesforce/dataloader/config/AppConfig.java @@ -763,7 +763,7 @@ private void setDefaults(Map cliOptionsMap) { setDefaultValue(PROP_UPDATE_WITH_EXTERNALID, false); setDefaultValue(PROP_DELETE_WITH_EXTERNALID, false); setDefaultValue(PROP_OAUTH_LOGIN_FROM_BROWSER, true); - setDefaultValue(PROP_OAUTH_LOGIN_FROM_BROWSER_DEVICE_OAUTH, true); + setDefaultValue(PROP_OAUTH_LOGIN_FROM_BROWSER_DEVICE_OAUTH, false); setDefaultValue(PROP_OAUTH_PKCE_PORT, DEFAULT_OAUTH_PKCE_PORT); setDefaultValue(PROP_LOAD_PRESERVE_WHITESPACE_IN_RICH_TEXT, true); setDefaultValue(CLI_OPTION_RUN_MODE, RUN_MODE_UI_VAL); diff --git a/src/main/java/com/salesforce/dataloader/controller/Controller.java b/src/main/java/com/salesforce/dataloader/controller/Controller.java index 5cc76544..d0402c59 100644 --- a/src/main/java/com/salesforce/dataloader/controller/Controller.java +++ b/src/main/java/com/salesforce/dataloader/controller/Controller.java @@ -345,6 +345,11 @@ public void createAndShowGUI() throws ControllerInitializationException { } public void updateLoaderWindowTitleAndCacheUserInfoForTheSession() { + // Skip UI updates in batch mode + if (AppUtil.getAppRunMode() == AppUtil.APP_RUN_MODE.BATCH) { + return; + } + if (isLoggedIn()) { try { ConnectorConfig sessionConfig = getPartnerClient().getConnection().getConfig(); diff --git a/src/main/java/com/salesforce/dataloader/oauth/OAuthFlowHandler.java b/src/main/java/com/salesforce/dataloader/oauth/OAuthFlowHandler.java new file mode 100644 index 00000000..004381ab --- /dev/null +++ b/src/main/java/com/salesforce/dataloader/oauth/OAuthFlowHandler.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2015, salesforce.com, inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + * the following disclaimer in the documentation and/or other materials provided with the distribution. + * + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.salesforce.dataloader.oauth; + +import com.salesforce.dataloader.config.AppConfig; +import com.salesforce.dataloader.controller.Controller; +import com.salesforce.dataloader.util.OAuthBrowserDeviceLoginRunner; +import com.salesforce.dataloader.util.OAuthBrowserFlow; +import com.salesforce.dataloader.util.DLLogManager; +import org.apache.logging.log4j.Logger; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; + +/** + * Utility class to handle OAuth flows (PKCE and Device Flow) for both UI and batch modes. + */ +public class OAuthFlowHandler { + private static final Logger logger = DLLogManager.getLogger(OAuthFlowHandler.class); + private final AppConfig appConfig; + private final Consumer statusConsumer; + private final Controller controller; + + public OAuthFlowHandler(AppConfig appConfig, Consumer statusConsumer, Controller controller) { + this.appConfig = appConfig; + this.statusConsumer = statusConsumer; + this.controller = controller; + } + + /** + * Handles the OAuth login process, attempting PKCE flow first if not disabled, + * falling back to device flow if PKCE is not supported or fails. + * + * @return true if login was successful, false otherwise + */ + public boolean handleOAuthLogin() { + logger.info("Starting OAuth flow"); + + // Check if device login from browser is explicitly enabled + String deviceLoginFromBrowser = appConfig.getString(AppConfig.PROP_OAUTH_LOGIN_FROM_BROWSER_DEVICE_OAUTH); + boolean deviceLoginFromBrowserEnabled = "true".equalsIgnoreCase(deviceLoginFromBrowser); + logger.debug("Device login from browser setting: " + deviceLoginFromBrowser); + logger.debug("Device login from browser enabled: " + deviceLoginFromBrowserEnabled); + + try { + // If device login from browser is explicitly enabled, use device flow directly + if (deviceLoginFromBrowserEnabled) { + logger.info("Device login from browser is enabled, using device flow"); + return handleDeviceFlow(); + } + + // If device login from browser is not enabled, try PKCE first + logger.debug("Device login from browser is not enabled, checking PKCE support"); + if (checkPKCESupport()) { + logger.info("PKCE is supported by connected app, attempting PKCE flow"); + OAuthBrowserFlow pkceFlow = new OAuthBrowserFlow(appConfig); + if (pkceFlow.performOAuthFlow()) { + logger.info("PKCE flow completed successfully"); + // Update controller's login state + if (controller != null) { + try { + if (controller.login()) { + controller.saveConfig(); + controller.updateLoaderWindowTitleAndCacheUserInfoForTheSession(); + return true; + } + } catch (Exception e) { + logger.error("Failed to update controller's login state", e); + return false; + } + } + return true; + } + logger.info("PKCE flow did not complete successfully, falling back to device flow"); + } + + // If PKCE failed or is not supported, try device flow + logger.info("Starting device flow"); + return handleDeviceFlow(); + + } catch (Exception e) { + logger.error("OAuth flow failed with unexpected error: " + e.getMessage(), e); + if (statusConsumer != null) { + statusConsumer.accept("OAuth flow failed. Please try again."); + } + return false; + } + } + + /** + * Checks if PKCE is supported by the connected app. + * + * @return true if PKCE is supported, false otherwise + */ + private boolean checkPKCESupport() { + try { + // Find an available port for PKCE + int pkcePort; + try (ServerSocket socket = new ServerSocket(0)) { + pkcePort = socket.getLocalPort(); + } catch (Exception e) { + logger.error("Failed to find available port for PKCE", e); + return false; + } + + // Check if PKCE is supported by making a test request + String testUrl = appConfig.getAuthEndpointForCurrentEnv() + "/services/oauth2/authorize"; + String clientId = appConfig.getEffectiveClientIdForCurrentEnv(); + String redirectUri = "http://localhost:" + pkcePort + "/OauthRedirect"; + + // Build test URL with PKCE parameters + StringBuilder testUrlBuilder = new StringBuilder(testUrl); + testUrlBuilder.append("?response_type=code"); + testUrlBuilder.append("&client_id=").append(URLEncoder.encode(clientId, StandardCharsets.UTF_8.name())); + testUrlBuilder.append("&redirect_uri=").append(URLEncoder.encode(redirectUri, StandardCharsets.UTF_8.name())); + testUrlBuilder.append("&scope=").append(URLEncoder.encode("api refresh_token", StandardCharsets.UTF_8.name())); + testUrlBuilder.append("&code_challenge=test"); + testUrlBuilder.append("&code_challenge_method=S256"); + + // Make a test request with PKCE parameters + URL url = new URL(testUrlBuilder.toString()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + try { + int responseCode = conn.getResponseCode(); + // If we get a 400 or 401, PKCE is not supported + if (responseCode == 400 || responseCode == 401) { + String errorResponse = readErrorResponse(conn); + if (errorResponse != null && ( + errorResponse.contains("redirect_uri_mismatch") || + errorResponse.contains("invalid_request") || + errorResponse.contains("unsupported_grant_type"))) { + logger.info("PKCE not supported by connected app (error: " + errorResponse + ")"); + return false; + } + } + return true; + } finally { + conn.disconnect(); + } + } catch (Exception e) { + logger.error("Error checking PKCE support: " + e.getMessage(), e); + return false; + } + } + + /** + * Handles the device flow OAuth process. + * + * @return true if device flow was successful, false otherwise + */ + private boolean handleDeviceFlow() { + try { + OAuthBrowserDeviceLoginRunner deviceFlow = new OAuthBrowserDeviceLoginRunner(appConfig, true); + + // Wait for login process to complete + while (deviceFlow.getLoginStatus() == OAuthBrowserDeviceLoginRunner.LoginStatus.WAIT) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + logger.error("Device flow interrupted", e); + return false; + } + } + + if (deviceFlow.getLoginStatus() == OAuthBrowserDeviceLoginRunner.LoginStatus.SUCCESS) { + logger.info("Device flow completed successfully"); + // Update controller's login state + if (controller != null) { + try { + if (controller.login()) { + controller.saveConfig(); + controller.updateLoaderWindowTitleAndCacheUserInfoForTheSession(); + return true; + } + } catch (Exception e) { + logger.error("Failed to update controller's login state", e); + return false; + } + } + if (statusConsumer != null) { + statusConsumer.accept("OAuth device flow completed successfully."); + } + return true; + } else { + logger.error("Device flow failed"); + if (statusConsumer != null) { + statusConsumer.accept("OAuth device flow failed. Please try again."); + } + return false; + } + } catch (Exception e) { + logger.error("Device flow failed with error: " + e.getMessage(), e); + if (statusConsumer != null) { + statusConsumer.accept("OAuth device flow failed. Please try again."); + } + return false; + } + } + + /** + * Reads the error response from an HTTP connection. + */ + private String readErrorResponse(HttpURLConnection conn) { + try { + InputStream in = conn.getErrorStream(); + if (in == null) { + return null; + } + StringBuilder response = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + return response.toString(); + } catch (Exception e) { + logger.debug("Error reading error response", e); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/salesforce/dataloader/process/ProcessRunner.java b/src/main/java/com/salesforce/dataloader/process/ProcessRunner.java index ccc04e76..7a404a88 100644 --- a/src/main/java/com/salesforce/dataloader/process/ProcessRunner.java +++ b/src/main/java/com/salesforce/dataloader/process/ProcessRunner.java @@ -37,7 +37,6 @@ import com.salesforce.dataloader.exception.OAuthBrowserLoginRunnerException; import com.salesforce.dataloader.exception.ParameterLoadException; import com.salesforce.dataloader.exception.ProcessInitializationException; -import com.salesforce.dataloader.ui.Labels; import com.salesforce.dataloader.util.AppUtil; import com.salesforce.dataloader.util.ExitException; import com.salesforce.dataloader.util.OAuthBrowserDeviceLoginRunner; @@ -46,9 +45,16 @@ import com.salesforce.dataloader.util.DLLogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.InitializingBean; -import com.salesforce.dataloader.ui.URLUtil; +import com.salesforce.dataloader.oauth.OAuthFlowHandler; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Calendar; import java.util.HashMap; import java.util.Map; @@ -67,10 +73,15 @@ public class ProcessRunner implements InitializingBean, IProcess { private String name; private final Map configOverrideMap = new HashMap<>(); - private Controller controller; + private final Controller controller; private ILoaderProgress monitor; + public ProcessRunner(Controller controller) { + this.controller = controller; + } + protected ProcessRunner() { + this.controller = null; } public synchronized void run(ILoaderProgress monitor) throws Exception { @@ -95,13 +106,26 @@ public synchronized void run(ILoaderProgress monitor) throws Exception { } private void initializeController() throws ControllerInitializationException, ParameterLoadException, ConfigInitializationException { - controller = Controller.getInstance(getConfigOverrideMap()); + if (controller == null) { + // In batch mode, create a new controller instance + Controller newController = Controller.getInstance(getConfigOverrideMap()); + if (newController == null) { + throw new ControllerInitializationException("Failed to initialize controller"); + } + // Use reflection to set the controller field since it's final + try { + java.lang.reflect.Field field = ProcessRunner.class.getDeclaredField("controller"); + field.setAccessible(true); + field.set(this, newController); + } catch (Exception e) { + throw new ControllerInitializationException("Failed to set controller: " + e.getMessage()); + } + } } private void handleOAuthLogin(AppConfig appConfig) throws OAuthBrowserLoginRunnerException { if (requiresOAuthLogin(appConfig)) { - if (appConfig.getBoolean(AppConfig.PROP_OAUTH_LOGIN_FROM_BROWSER_DEVICE_OAUTH)) { - doDeviceLoginFromBrowser(appConfig); + if (doDeviceLoginFromBrowser(appConfig)) { } else { doBrowserLogin(appConfig); } @@ -145,7 +169,7 @@ private void saveLastRunDate(AppConfig appConfig) throws IOException { } private void closeResources() { - if (controller.getDao() != null) { + if (controller != null && controller.getDao() != null) { controller.getDao().close(); } } @@ -294,36 +318,22 @@ private static void validateConfigProperties(AppConfig appConfig) throws Process } } - void doDeviceLoginFromBrowser(AppConfig appConfig) throws OAuthBrowserLoginRunnerException { + private boolean doDeviceLoginFromBrowser(AppConfig appConfig) { + logger.info("Starting OAuth login from browser"); try { - logger.debug("Starting OAuth device flow..."); - if (AppUtil.getAppRunMode() == AppUtil.APP_RUN_MODE.BATCH) { - logger.debug("Running in batch mode - please complete authentication in your browser"); - } else { - logger.debug("A browser window will open for you to log in to Salesforce"); - } - - OAuthBrowserDeviceLoginRunner loginRunner = new OAuthBrowserDeviceLoginRunner(appConfig, true); - String verificationURLStr = loginRunner.getVerificationURLStr(); - logger.info("Please complete the authentication process in your browser"); - logger.info("Verification URL: " + verificationURLStr); - logger.info("User Code: " + loginRunner.getUserCode()); - - // Try to open browser in both UI and batch modes - URLUtil.openURL(verificationURLStr); - - while (!loginRunner.isLoginProcessCompleted()) { - Thread.sleep(1000); + if (controller == null) { + logger.error("Controller is not initialized"); + return false; } - - if (loginRunner.getLoginStatus() == OAuthBrowserDeviceLoginRunner.LoginStatus.SUCCESS) { - logger.debug("OAuth device flow completed successfully!"); - } else { - throw new OAuthBrowserLoginRunnerException("OAuth device flow failed - authentication could not be completed"); - } - } catch (Exception ex) { - logger.error("OAuth device flow failed: " + ex.getMessage()); - throw new OAuthBrowserLoginRunnerException("OAuth device flow failed: " + ex.getMessage()); + OAuthFlowHandler oauthHandler = new OAuthFlowHandler(appConfig, status -> { + if (status != null) { + logger.info(status); + } + }, controller); + return oauthHandler.handleOAuthLogin(); + } catch (Exception e) { + logger.error("OAuth login failed with error: " + e.getMessage(), e); + return false; } } @@ -333,11 +343,15 @@ void doBrowserLogin(AppConfig appConfig) throws OAuthBrowserLoginRunnerException logger.debug("A browser window will open for you to log in to Salesforce."); logger.debug("Please complete your login in the browser window."); - OAuthBrowserFlow oauthFlow = new OAuthBrowserFlow(appConfig); - boolean success = oauthFlow.performOAuthFlow(); + if (controller == null) { + throw new OAuthBrowserLoginRunnerException("Controller is not initialized"); + } + + OAuthFlowHandler oauthHandler = new OAuthFlowHandler(appConfig, status -> logger.debug(status), controller); + boolean success = oauthHandler.handleOAuthLogin(); if (success) { - logger.debug("OAuth browser login completed successfully!"); + logger.debug("OAuth browser login completed successfully!"); } else { throw new OAuthBrowserLoginRunnerException("OAuth browser login failed - authentication could not be completed"); } diff --git a/src/main/java/com/salesforce/dataloader/ui/AuthenticationRunner.java b/src/main/java/com/salesforce/dataloader/ui/AuthenticationRunner.java index 4f78e2a4..1b3dd35f 100644 --- a/src/main/java/com/salesforce/dataloader/ui/AuthenticationRunner.java +++ b/src/main/java/com/salesforce/dataloader/ui/AuthenticationRunner.java @@ -34,14 +34,11 @@ import com.salesforce.dataloader.util.OAuthBrowserFlow; import com.sforce.soap.partner.PartnerConnection; import com.sforce.soap.partner.QueryResult; -import com.sforce.soap.partner.fault.LoginFault; -import com.sforce.soap.partner.fault.UnexpectedErrorFault; import com.sforce.soap.partner.sobject.SObject; import com.sforce.ws.ConnectionException; import com.sforce.ws.bind.XmlObject; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.Level; import com.salesforce.dataloader.util.DLLogManager; import org.eclipse.swt.custom.BusyIndicator; @@ -50,6 +47,15 @@ import java.util.Iterator; import java.util.function.Consumer; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.net.ServerSocket; +import com.salesforce.dataloader.oauth.OAuthFlowHandler; /** * AuthenticationRunner is the UI orchestration of logging in. @@ -86,111 +92,54 @@ public void login(LoginCriteria criteria, Consumer messenger) { BusyIndicator.showWhile(Display.getDefault(), new Thread(this::loginAsync)); } - private void loginAsync(){ + private void loginAsync() { try { - authStatusChangeConsumer.accept(Labels.getString("LoginPage.verifyingLogin")); - logger.info(Labels.getString("LoginPage.verifyingLogin")); - - // Check if browser session login is configured by default - if (criteria.getMode() == LoginCriteria.OAuthLogin) { - if (appConfig.getBoolean(AppConfig.PROP_OAUTH_LOGIN_FROM_BROWSER_DEVICE_OAUTH)) { - OAuthDeviceFlow flow = new OAuthDeviceFlow(shell, appConfig); - if (!flow.open()) { - String message = Labels.getString("LoginPage.invalidLoginOAuthBrowser"); - authStatusChangeConsumer.accept(message); - return; - } - } else if (appConfig.getBoolean(AppConfig.PROP_OAUTH_LOGIN_FROM_BROWSER)) { - // Handle proper OAuth browser flow instead of session capture - authStatusChangeConsumer.accept("Starting OAuth browser authentication..."); - logger.info("Using OAuth browser authentication mode"); - OAuthBrowserFlow oauthFlow = new OAuthBrowserFlow(appConfig); - if (!oauthFlow.performOAuthFlow()) { - String message = "OAuth browser authentication failed. Please try again."; - authStatusChangeConsumer.accept(message); - return; - } - authStatusChangeConsumer.accept("OAuth browser authentication successful."); - } else { // OAuth login from Data Loader app - // Check for External Client App configuration first - String ecaValidationMessage = appConfig.validateExternalClientAppConfig(); - if (ecaValidationMessage != null) { - authStatusChangeConsumer.accept(ecaValidationMessage); - return; - } - - boolean hasSecret = !appConfig.getEffectiveClientSecretForCurrentEnv().trim().equals(""); - if (appConfig.isExternalClientAppConfigured()) { - logger.info("Using External Client App for OAuth authentication"); - // ECA requires client secret, so use OAuthSecretFlow - OAuthFlow flow = new OAuthSecretFlow(shell, appConfig); - if (!flow.open()) { - String message = Labels.getString("LoginPage.invalidLoginOAuth"); - if (flow.getStatusCode() == HttpTransportInterface.PROXY_AUTHENTICATION_REQUIRED) { - message = Labels.getFormattedString("LoginPage.proxyError", flow.getReasonPhrase()); - } - authStatusChangeConsumer.accept(message); - return; - } - } else { - logger.info("Using Connected App for OAuth authentication"); - OAuthFlow flow = hasSecret ? new OAuthSecretFlow(shell, appConfig) : new OAuthTokenFlow(shell, appConfig); - if (!flow.open()) { - String message = Labels.getString("LoginPage.invalidLoginOAuth"); - if (flow.getStatusCode() == HttpTransportInterface.PROXY_AUTHENTICATION_REQUIRED) { - message = Labels.getFormattedString("LoginPage.proxyError", flow.getReasonPhrase()); - } - - if (flow.getReasonPhrase() == null) { - logger.info("OAuth login dialog closed without logging in"); - } else { - logger.info("Login failed:" + flow.getReasonPhrase()); - } - authStatusChangeConsumer.accept(message); - return; - } - } + // Check if OAuth is required + if (requiresOAuthLogin(appConfig)) { + OAuthFlowHandler oauthHandler = new OAuthFlowHandler(appConfig, this::updateStatus, controller); + if (oauthHandler.handleOAuthLogin()) { + updateStatus("OAuth login successful"); + return; } - } - } catch (Throwable e) { - handleError(e, e.getMessage()); - return; + updateStatus("OAuth login failed"); + return; + } + + // If OAuth is not required, proceed with username/password login + updateStatus("Logging in..."); + PartnerConnection conn = login(); + if (conn != null) { + updateStatus("Login successful"); + } else { + updateStatus("Login failed"); + } + } catch (Exception e) { + logger.error("Login failed", e); + updateStatus("Login failed: " + e.getMessage()); } - // Either OAuth login is successful or - // need to perform username and password or session token based auth + } + + private void updateStatus(String status) { + authStatusChangeConsumer.accept(status); + } + + private boolean requiresOAuthLogin(AppConfig config) { + return criteria.getMode() == LoginCriteria.OAuthLogin; + } + + private PartnerConnection login() { try { if (controller.login() && controller.getEntityDescribes() != null) { controller.saveConfig(); controller.updateLoaderWindowTitleAndCacheUserInfoForTheSession(); - PartnerConnection conn = controller.getPartnerClient().getConnection(); - logger.debug("org_id = " + conn.getUserInfo().getOrganizationId()); - logger.debug("user_id = " + conn.getUserInfo().getUserId()); - if (logger.getLevel() == Level.DEBUG) { - // avoid making a remote API call to the server unless log level is DEBUG - logger.debug(getSoqlResultsAsString( - "\nConnected App Information: ", - "SELECT Name, id FROM ConnectedApplication WHERE name like 'Dataloader%'" - , conn)); - logger.debug(getSoqlResultsAsString( - "\nOrg Instance Information:", - "SELECT InstanceName FROM Organization" - , conn)); - } - authStatusChangeConsumer.accept(Labels.getString("LoginPage.loginSuccessful")); - } else { - authStatusChangeConsumer.accept(Labels.getString("LoginPage.invalidLoginUsernamePassword")); + return controller.getLoginClient().getConnection(); } - } catch (LoginFault lf) { - handleError(lf, Labels.getString("LoginPage.invalidLoginUsernamePassword")); - } catch (UnexpectedErrorFault e) { - handleError(e, e.getExceptionMessage()); - } catch (ConnectionException e) { - // TODO Auto-generated catch block - handleError(e, e.getMessage()); - } - + } catch (Exception e) { + logger.error("Error during username+password login", e); + } + return null; } - + private String getSoqlResultsAsString(String prefix, String soql, PartnerConnection conn) throws ConnectionException { QueryResult result = conn.query(soql); final SObject[] sfdcResults = result.getRecords(); @@ -238,4 +187,15 @@ private void handleError(Throwable e, String message) { } logger.debug("\n" + ExceptionUtil.getStackTraceString(e)); } + + private String readErrorResponse(HttpURLConnection conn) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + return response.toString(); + } + } }