Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<groupId>com.force.api</groupId>
<artifactId>force-wsc</artifactId>
<packaging>jar</packaging>
<version>64.0.0_13</version>
<version>66.0.0_1</version>
<name>force-wsc</name>
<description>Force.com Web Service Connector</description>
<url>http://www.force.com</url>
Expand Down Expand Up @@ -74,7 +74,7 @@
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
Expand Down Expand Up @@ -151,8 +151,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
Expand Down Expand Up @@ -197,8 +197,8 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>17</source>
<target>17</target>
<doclint>none</doclint>
<additionalJOption>-Xdoclint:none</additionalJOption>
</configuration>
Expand Down
195 changes: 132 additions & 63 deletions src/main/java/com/sforce/bulk/LoginHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,105 +27,174 @@
package com.sforce.bulk;

import com.sforce.ws.ConnectionException;
import com.sforce.ws.transport.Transport;
import com.sforce.ws.util.FileUtil;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
* This class is a helper to do login using partner wsdl.
*
* This class is a helper to authenticate using the OAuth2 username-password flow.
* The Salesforce SOAP Partner API login() call was removed from API versions 65.0
* and later; this implementation uses the /services/oauth2/token endpoint instead.
*
* <p/>
* User: mcheenath
* Date: Dec 10, 2010
*/
public class LoginHelper {

private StreamHandler handler;
private static final String SALESFORCE_VERSION = "66.0";
private static final String GRANT_TYPE_PASSWORD = "grant_type=password";
private static final String PARAM_CLIENT_ID = "&client_id=";
private static final String PARAM_CLIENT_SECRET = "&client_secret=";
private static final String PARAM_USERNAME = "&username=";
private static final String PARAM_PASSWORD = "&password=";
private static final String HTTP_METHOD_POST = "POST";
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded";
private static final String JSON_FIELD_ACCESS_TOKEN = "access_token";
private static final String JSON_FIELD_INSTANCE_URL = "instance_url";

private final StreamHandler handler;

LoginHelper(StreamHandler handler) {
this.handler = handler;
}

void doLogin() throws IOException, StreamException {
handler.info("Calling login on: " + handler.getConfig().getAuthEndpoint());

String request = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<env:Envelope xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
"xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<env:Body><m:login xmlns:m=\"urn:partner.soap.sforce.com\" " +
"xmlns:sobj=\"urn:sobject.partner.soap.sforce.com\">" +
"<m:username>" +
handler.getConfig().getUsername() +
"</m:username>" +
"<m:password>" +
handler.getConfig().getPassword() +
"</m:password>" +
"</m:login>" +
"</env:Body>" +
"</env:Envelope>";

Transport transport;
try {
transport = handler.getConfig().createTransport();
} catch (ConnectionException x) {
throw new IOException(String.format("Cannot create transport %s", handler.getConfig().getTransport()), x);
String authEndpoint = handler.getConfig().getAuthEndpoint();
if (authEndpoint == null || authEndpoint.isEmpty()) {
throw new StreamException("authEndpoint is required. Set it via ConnectorConfig.setAuthEndpoint().");
}
OutputStream out = transport.connect(handler.getConfig().getAuthEndpoint(), "");
out.write(request.getBytes());
out.close();

InputStream input = transport.getContent();
String response = new String(FileUtil.toBytes(input));

String sessionId = getValueForTag("sessionId", response);
handler.getConfig().setSessionId(sessionId);
handler.info("Session Id: " + sessionId);

String serverUrl = getValueForTag("serverUrl", response);
String username = handler.getConfig().getUsername();
if (username == null || username.isEmpty()) {
throw new StreamException("username is required. Set it via ConnectorConfig.setUsername().");
}

if (sessionId == null || serverUrl == null) {
throw new StreamException("Failed to login " + response);
String password = handler.getConfig().getPassword();
if (password == null || password.isEmpty()) {
throw new StreamException("password is required. Set it via ConnectorConfig.setPassword().");
}

setBulkUrl(response, serverUrl);
}
String baseUrl = deriveBaseUrl(authEndpoint);
String apiVersion = deriveApiVersion(authEndpoint);
String tokenUrl = baseUrl + "/services/oauth2/token";

private void setBulkUrl(String response, String serverUrl) throws StreamException {
String partnerTag = "/services/Soap/u/";
handler.info("Calling OAuth2 login on: " + tokenUrl);

int index = serverUrl.indexOf(partnerTag);
String clientId = handler.getConfig().getClientId();
String clientSecret = handler.getConfig().getClientSecret();

if (index == -1) {
throw new StreamException("Unknown serverUrl " + serverUrl + "in response " + response);
if (clientId == null || clientId.isEmpty()) {
throw new StreamException(
"clientId is required for OAuth2 authentication. " +
"Set it via ConnectorConfig.setClientId() with your Connected App consumer key.");
}

String bulkUrl = serverUrl.substring(0, index);
String body = GRANT_TYPE_PASSWORD
+ PARAM_CLIENT_ID + URLEncoder.encode(clientId, StandardCharsets.UTF_8)
+ PARAM_CLIENT_SECRET + URLEncoder.encode(clientSecret != null ? clientSecret : "", StandardCharsets.UTF_8)
+ PARAM_USERNAME + URLEncoder.encode(username, StandardCharsets.UTF_8)
+ PARAM_PASSWORD + URLEncoder.encode(password, StandardCharsets.UTF_8);

int verIndex = index + partnerTag.length();
String version = serverUrl.substring(verIndex, verIndex+4);
URL url;
try {
url = new URL(tokenUrl);
} catch (MalformedURLException e) {
throw new StreamException("Invalid OAuth2 token URL: " + tokenUrl);
}

bulkUrl = bulkUrl + "/services/async/" + version + "/";
HttpURLConnection conn = handler.getConfig().createConnection(url, null, false);
try {
conn.setRequestMethod(HTTP_METHOD_POST);
conn.setRequestProperty(HEADER_CONTENT_TYPE, CONTENT_TYPE_FORM_URLENCODED);
conn.setDoOutput(true);

handler.getConfig().setRestEndpoint(bulkUrl);
handler.info("Bulk API Server Url :" + bulkUrl);
}
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}

private String getValueForTag(String tag, String response) {
String value = null;
int index = response.indexOf("<" + tag + ">");
int status = conn.getResponseCode();
InputStream errorStream = conn.getErrorStream();
InputStream responseStream = status == 200 ? conn.getInputStream() : errorStream;
byte[] responseBytes = responseStream != null ? FileUtil.toBytes(responseStream) : new byte[0];
String response = new String(responseBytes, StandardCharsets.UTF_8);

if (index != -1) {
int end = response.indexOf("</" + tag + ">");
if (status != 200) {
throw new StreamException("OAuth2 login failed (HTTP " + status + "): " + response);
}

if (end != -1) {
value = response.substring(index + tag.length() + 2, end);
String accessToken = extractJsonStringValue(JSON_FIELD_ACCESS_TOKEN, response);
String instanceUrl = extractJsonStringValue(JSON_FIELD_INSTANCE_URL, response);

if (accessToken == null || instanceUrl == null) {
throw new StreamException("Failed to parse OAuth2 response: " + response);
}

handler.getConfig().setSessionId(accessToken);
handler.info("Access token obtained successfully.");

String bulkUrl = instanceUrl + "/services/async/" + apiVersion + "/";
handler.getConfig().setRestEndpoint(bulkUrl);
handler.info("Bulk API Server Url: " + bulkUrl);
} finally {
conn.disconnect();
}
}

/**
* Extracts the base URL (scheme + host + optional port) from a SOAP auth endpoint such as
* https://login.salesforce.com/services/Soap/u/66.0, or falls back to URL parsing.
*/
private String deriveBaseUrl(String authEndpoint) throws StreamException {
int idx = authEndpoint.indexOf("/services/");
if (idx != -1) {
return authEndpoint.substring(0, idx);
}
try {
URL u = new URL(authEndpoint);
int port = u.getPort();
return u.getProtocol() + "://" + u.getHost() + (port != -1 ? ":" + port : "");
} catch (MalformedURLException e) {
throw new StreamException("Cannot derive base URL from authEndpoint: " + authEndpoint);
}
}

/**
* Extracts the API version from a SOAP auth endpoint such as
* https://login.salesforce.com/services/Soap/u/66.0 → "66.0".
*/
private String deriveApiVersion(String authEndpoint) {
String soapPath = "/services/Soap/u/";
int idx = authEndpoint.indexOf(soapPath);
if (idx != -1) {
String tail = authEndpoint.substring(idx + soapPath.length());
int slash = tail.indexOf('/');
return slash != -1 ? tail.substring(0, slash) : tail;
}
return SALESFORCE_VERSION;
}

return value;
/**
* Minimal JSON string-value extractor for flat OAuth2 responses.
* Handles escaped quotes within the value are not expected in these fields.
*/
private String extractJsonStringValue(String key, String json) {
String search = "\"" + key + "\"";
int idx = json.indexOf(search);
if (idx == -1) return null;
int colon = json.indexOf(':', idx + search.length());
if (colon == -1) return null;
int start = json.indexOf('"', colon + 1);
if (start == -1) return null;
int end = json.indexOf('"', start + 1);
if (end == -1) return null;
return json.substring(start + 1, end);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/sforce/ws/ConnectorConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ public void close() throws IOException {
private boolean useChunkedPost;
private String username;
private String password;
private String clientId;
private String clientSecret;
private String sessionId;
private String authEndpoint;
private String serviceEndpoint;
Expand Down Expand Up @@ -289,6 +291,22 @@ public void setPassword(String password) {
this.password = password;
}

public String getClientId() {
return clientId;
}

public void setClientId(String clientId) {
this.clientId = clientId;
}

public String getClientSecret() {
return clientSecret;
}

public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}

public String getSessionId() {
return sessionId;
}
Expand Down
Loading