diff --git a/src/main/java/com/sforce/jwt/JwtConnection.java b/src/main/java/com/sforce/jwt/JwtConnection.java new file mode 100644 index 00000000..b5dc48c3 --- /dev/null +++ b/src/main/java/com/sforce/jwt/JwtConnection.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.SocketTimeoutException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sforce.ws.ConnectorConfig; +import com.sforce.ws.transport.Transport; + +public class JwtConnection { + + private JwtTokenBuilder jwtTokenBuilder; + private ConnectorConfig config; + private String jwtUrl; + + public JwtConnection(ConnectorConfig config) { + this.config = config; + this.jwtUrl = config.getJwtLoginUrl(); + this.jwtTokenBuilder = new JwtTokenBuilder(); + } + + public JwtResult send() throws JwtConnectionException { + long startTime = System.currentTimeMillis(); + try { + + Transport transport = newTransport(config); + + HashMap httpHeaders = new HashMap<>(); + httpHeaders.put("Content-Type", "application/x-www-form-urlencoded"); + OutputStream out = jwtConnect(transport, jwtUrl, httpHeaders, false); + Map parameters = buildJwtUrlParameters(); + sendRequest(out, buildParamData(parameters)); + InputStream in = transport.getContent(); + JwtResponse jwtResponse = receive(transport, in); + + JwtResult jwtResult = new JwtResult(); + jwtResult.setJwtResponse(jwtResponse); + jwtResult.setServerUrl(buildServiceEndpoint(jwtResponse)); + + return jwtResult; + + } catch (SocketTimeoutException e) { + long timeTaken = System.currentTimeMillis() - startTime; + throw new JwtConnectionException("Request to " + jwtUrl + " timed out. TimeTaken=" + timeTaken + " ConnectionTimeout=" + + config.getConnectionTimeout() + " ReadTimeout=" + config.getReadTimeout(), e); + } catch (IOException e) { + throw new JwtConnectionException("Failed to send request to " + jwtUrl, e); + } + } + + protected Map buildJwtUrlParameters() throws JwtConnectionException { + String token = jwtTokenBuilder.build(config); + Map parameters = new LinkedHashMap<>(); + parameters.put("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); + parameters.put("assertion", token); + return parameters; + } + + protected OutputStream jwtConnect(Transport transport, String uri, HashMap httpHeaders, boolean enableCompression) + throws IOException { + return transport.connect(uri, httpHeaders, enableCompression); + } + + protected byte[] buildParamData(Map parameters) throws UnsupportedEncodingException { + StringBuilder data = new StringBuilder(); + for (Map.Entry p : parameters.entrySet()) { + if (data.length() != 0) { + data.append('&'); + } + data.append(URLEncoder.encode(p.getKey(), StandardCharsets.UTF_8.toString())); + data.append('='); + data.append(URLEncoder.encode(String.valueOf(p.getValue()), StandardCharsets.UTF_8.toString())); + } + return data.toString().getBytes(StandardCharsets.UTF_8.toString()); + } + + protected String buildServiceEndpoint(JwtResponse jwtResponse) { + String authUrl = config.getAuthEndpoint(); + String path = findCurrentEndPointPath(authUrl); + String tail = authUrl.substring(authUrl.indexOf(path) + path.length()); + String version = tail.contains("/") ? tail.substring(0, tail.indexOf("/")) : tail; + return jwtResponse.getInstance_url() + path + version + "/"; + } + + private String findCurrentEndPointPath(String inputUrl) { + if (inputUrl.contains(ConnectorConfig.PARTNER_ENDPOINT_PATH)) { + return ConnectorConfig.PARTNER_ENDPOINT_PATH; + } else if (inputUrl.contains(ConnectorConfig.TOOLING_ENDPOINT_PATH)) { + return ConnectorConfig.TOOLING_ENDPOINT_PATH; + } else { + return ConnectorConfig.ENTERPRISE_ENDPOINT_PATH; + } + } + + private void sendRequest(OutputStream out, byte[] dataBytes) throws IOException { + try (DataOutputStream outputStream = new DataOutputStream(out)) { + outputStream.write(dataBytes); + } + } + + private JwtResponse receive(Transport transport, InputStream in) throws IOException, JwtConnectionException { + Reader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8.toString())); + StringBuilder stringBuilder = new StringBuilder(); + for (int chr; (chr = bufferedReader.read()) >= 0;) { + stringBuilder.append((char) chr); + } + String jwtResponseJson = stringBuilder.toString(); + JwtResponse jwtResponse = new ObjectMapper().readValue(jwtResponseJson, JwtResponse.class); + if (transport.isSuccessful()) { + return jwtResponse; + } else { + throw createException(jwtResponse); + } + } + + private JwtConnectionException createException(JwtResponse jwtResponse) { + if (jwtResponse == null || jwtResponse.getError() == null || jwtResponse.getError().trim().isEmpty()) { + return new JwtConnectionException("Unknown JWT connection exception"); + } + String error = jwtResponse.getError(); + String desc = jwtResponse.getError_description() != null && !jwtResponse.getError_description().trim().isEmpty() // + ? jwtResponse.getError_description() // + : "unknown"; // + String exceptionMessage = exceptionMessageFormat(error, desc); + return new JwtConnectionException(exceptionMessage); + } + + private String exceptionMessageFormat(String error, String description) { + return new MessageFormat("{0} - {1}").format(new String[] { error, description }); + } + + private Transport newTransport(ConnectorConfig config) throws JwtConnectionException { + if (config.getTransportFactory() != null) { + Transport t = config.getTransportFactory().createTransport(); + t.setConfig(config); + return t; + } + try { + Transport t = (Transport) config.getTransport().newInstance(); + t.setConfig(config); + return t; + } catch (InstantiationException | IllegalAccessException e) { + throw new JwtConnectionException("Failed to create new Transport " + config.getTransport()); + } + } + +} diff --git a/src/main/java/com/sforce/jwt/JwtConnectionException.java b/src/main/java/com/sforce/jwt/JwtConnectionException.java new file mode 100644 index 00000000..a6efa133 --- /dev/null +++ b/src/main/java/com/sforce/jwt/JwtConnectionException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import com.sforce.ws.ConnectionException; + +public class JwtConnectionException extends ConnectionException { + + private static final long serialVersionUID = -6980997197068666839L; + + public JwtConnectionException() { + } + + public JwtConnectionException(String message) { + super(message); + } + + public JwtConnectionException(String message, Throwable th) { + super(message, th); + } +} diff --git a/src/main/java/com/sforce/jwt/JwtLoginService.java b/src/main/java/com/sforce/jwt/JwtLoginService.java new file mode 100644 index 00000000..53f0eb74 --- /dev/null +++ b/src/main/java/com/sforce/jwt/JwtLoginService.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import com.sforce.ws.ConnectorConfig; + +public class JwtLoginService { + + private ConnectorConfig config; + + public JwtLoginService(ConnectorConfig config) { + super(); + this.config = config; + } + + public JwtResult login() throws JwtConnectionException { + JwtConnection connection = newConnection(config); + return connection.send(); + } + + protected JwtConnection newConnection(ConnectorConfig config) { + return new JwtConnection(config); + } +} diff --git a/src/main/java/com/sforce/jwt/JwtResponse.java b/src/main/java/com/sforce/jwt/JwtResponse.java new file mode 100644 index 00000000..b56ca545 --- /dev/null +++ b/src/main/java/com/sforce/jwt/JwtResponse.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class JwtResponse { + + private String access_token; + private String scope; + private String instance_url; + private String id; + private String token_type; + private String error; + private String error_description; + + public JwtResponse() { + super(); + } + + public JwtResponse(String access_token, String scope, String instance_url, String id, String token_type, String error, String error_description) { + super(); + this.access_token = access_token; + this.scope = scope; + this.instance_url = instance_url; + this.id = id; + this.token_type = token_type; + this.error = error; + this.error_description = error_description; + } + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getInstance_url() { + return instance_url; + } + + public void setInstance_url(String instance_url) { + this.instance_url = instance_url; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getError_description() { + return error_description; + } + + public void setError_description(String error_description) { + this.error_description = error_description; + } + +} \ No newline at end of file diff --git a/src/main/java/com/sforce/jwt/JwtResult.java b/src/main/java/com/sforce/jwt/JwtResult.java new file mode 100644 index 00000000..f3f9d959 --- /dev/null +++ b/src/main/java/com/sforce/jwt/JwtResult.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties +public class JwtResult { + + private JwtResponse jwtResponse; + private String serverUrl; + + public JwtResponse getJwtResponse() { + return jwtResponse; + } + + public void setJwtResponse(JwtResponse jwtResponse) { + this.jwtResponse = jwtResponse; + } + + public String getServerUrl() { + return serverUrl; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + +} \ No newline at end of file diff --git a/src/main/java/com/sforce/jwt/JwtTokenBuilder.java b/src/main/java/com/sforce/jwt/JwtTokenBuilder.java new file mode 100644 index 00000000..f41a1047 --- /dev/null +++ b/src/main/java/com/sforce/jwt/JwtTokenBuilder.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.text.MessageFormat; +import java.util.Base64; +import java.util.UUID; + +import com.sforce.ws.ConnectorConfig; + +public class JwtTokenBuilder { + + public String build(ConnectorConfig config) throws JwtConnectionException { + + String header = "{\"alg\":\"RS256\"}"; + String claimTemplate = "'{'\"iss\": \"{0}\", \"sub\": \"{1}\", \"aud\": \"{2}\", \"exp\": \"{3}\", \"jti\": \"{4}\"'}'"; + + try { + + if(config.getJwtPrivateKey() == null) { + throw new JwtConnectionException("A private key is required for JWT authentication token building"); + } + + StringBuffer token = new StringBuffer(); + + //Encode the JWT Header and add it to our string to sign + token.append(Base64.getUrlEncoder().encodeToString(header.getBytes(StandardCharsets.UTF_8.toString()))); + + //Separate with a period + token.append("."); + + //Create the JWT Claims Object + String[] claimArray = new String[5]; + claimArray[0] = config.getClientId(); + claimArray[1] = config.getUsername(); + claimArray[2] = config.getJwtEndPointUrl(); + claimArray[3] = Long.toString((System.currentTimeMillis() / 1000) + 300); + claimArray[4] = UUID.randomUUID().toString(); + MessageFormat claims; + claims = new MessageFormat(claimTemplate); + String payload = claims.format(claimArray); + + //Add the encoded claims object + token.append(Base64.getUrlEncoder().encodeToString(payload.getBytes(StandardCharsets.UTF_8.toString()))); + + //Load the private key + PrivateKey privateKey = config.getJwtPrivateKey(); + + //Sign the JWT Header + "." + JWT Claims Object + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(privateKey); + signature.update(token.toString().getBytes(StandardCharsets.UTF_8.toString())); + String signedPayload = Base64.getUrlEncoder().encodeToString(signature.sign()); + + //Separate with a period + token.append("."); + + //Add the encoded signature + token.append(signedPayload); + + return token.toString(); + + } catch (NoSuchAlgorithmException | IOException | InvalidKeyException | SignatureException e) { + throw new JwtConnectionException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/sforce/ws/ConnectorConfig.java b/src/main/java/com/sforce/ws/ConnectorConfig.java index aa538a15..4a403cc1 100644 --- a/src/main/java/com/sforce/ws/ConnectorConfig.java +++ b/src/main/java/com/sforce/ws/ConnectorConfig.java @@ -37,6 +37,7 @@ import javax.net.ssl.SSLContext; import java.io.*; import java.net.*; +import java.security.PrivateKey; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -52,6 +53,10 @@ */ public class ConnectorConfig { + public static final String ENTERPRISE_ENDPOINT_PATH = "/services/Soap/c/"; + public static final String PARTNER_ENDPOINT_PATH = "/services/Soap/u/"; + public static final String TOOLING_ENDPOINT_PATH = "/services/Soap/T/"; + private MessageCaptureHandler captureHtmlHandler; public class TeeInputStream { @@ -145,6 +150,10 @@ public void close() throws IOException { private boolean useChunkedPost; private String username; private String password; + private String clientId; + private PrivateKey jwtPrivateKey; + private String jwtLoginUrl; + private String jwtEndPointUrl; private String sessionId; private String authEndpoint; private String serviceEndpoint; @@ -402,15 +411,15 @@ public boolean useChunkedPost() { } public void verifyPartnerEndpoint() throws ConnectionException { - verifyEndpoint("/services/Soap/u/"); + verifyEndpoint(PARTNER_ENDPOINT_PATH); } public void verifyEnterpriseEndpoint() throws ConnectionException { - verifyEndpoint("/services/Soap/c/"); + verifyEndpoint(ENTERPRISE_ENDPOINT_PATH); } public void verifyToolingEndpoint() throws ConnectionException { - verifyEndpoint("/services/Soap/T/"); + verifyEndpoint(TOOLING_ENDPOINT_PATH); } public Iterator getMessagerHandlers() { @@ -564,4 +573,37 @@ public MessageCaptureHandler getCaptureHtmlHandler() { private static boolean javaVersionHasABug() { return JavaVersion.javaVersionHasABug(System.getProperty(JavaVersion.JAVA_VERSION_PROPERTY)); } + + public PrivateKey getJwtPrivateKey() { + return jwtPrivateKey; + } + + public void setJwtPrivateKey(PrivateKey jwtPrivateKey) { + this.jwtPrivateKey = jwtPrivateKey; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getJwtLoginUrl() { + return jwtLoginUrl; + } + + public void setJwtLoginUrl(String jwtLoginUrl) { + this.jwtLoginUrl = jwtLoginUrl; + } + + public String getJwtEndPointUrl() { + return jwtEndPointUrl; + } + + public void setJwtEndPointUrl(String jwtEndPointUrl) { + this.jwtEndPointUrl = jwtEndPointUrl; + } + } diff --git a/src/main/java/com/sforce/ws/codegen/templates/connection.st b/src/main/java/com/sforce/ws/codegen/templates/connection.st index 569aa6ea..ce1e63f8 100644 --- a/src/main/java/com/sforce/ws/codegen/templates/connection.st +++ b/src/main/java/com/sforce/ws/codegen/templates/connection.st @@ -56,9 +56,15 @@ $header.elements: { ael | if (!config.isManualLogin()) { if (config.getSessionId()==null) { config.setServiceEndpoint(config.getAuthEndpoint()); - $gen.loginResult$ result = login(config.getUsername(), config.getPassword()); - config.setSessionId(result.getSessionId()); - config.setServiceEndpoint(result.getServerUrl()); + if(config.getJwtPrivateKey() != null) { + com.sforce.jwt.JwtResult jwtResult = new com.sforce.jwt.JwtLoginService(config).login(); + config.setSessionId(jwtResult.getJwtResponse().getAccess_token()); + config.setServiceEndpoint(jwtResult.getServerUrl()); + } else { + $gen.loginResult$ result = login(config.getUsername(), config.getPassword()); + config.setSessionId(result.getSessionId()); + config.setServiceEndpoint(result.getServerUrl()); + } } else { if (config.getServiceEndpoint() == null) { throw new com.sforce.ws.ConnectionException("Please set ServiceEndpoint"); diff --git a/src/test/java/com/sforce/jwt/JwtConnectionTest.java b/src/test/java/com/sforce/jwt/JwtConnectionTest.java new file mode 100644 index 00000000..56326c1b --- /dev/null +++ b/src/test/java/com/sforce/jwt/JwtConnectionTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.Iterator; + +import org.junit.Test; + +import com.sforce.ws.ConnectionException; +import com.sforce.ws.ConnectorConfig; +import com.sforce.ws.MessageHandler; +import com.sforce.ws.transport.Transport; +import com.sforce.ws.transport.TransportFactory; + +public class JwtConnectionTest { + + private static final String ACCESS_TOKEN = "ACCESS_TOKEN"; + private static final String INSTANCE_URL = "https://bulgari--env.sandbox.my.salesforce.com"; + private static final String ERROR = "ERROR_TYPE"; + private static final String ERROR_DESC = "ERROR_DESC"; + private static final String RESPONSE_JSON = "{" + // + "\"access_token\": \"" + ACCESS_TOKEN + "\"," + // + "\"scope\": \"id api\"," + // + "\"instance_url\": \"" + INSTANCE_URL + "\"," + // + "\"id\": \"https://test.salesforce.com/id/SESSION_ID\"," + // + "\"token_type\": \"Bearer\"" + // + "}"; + private static final String RESPONSE_JSON_ERROR = "{" + // + "\"error\": \"" + ERROR + "\"," + // + "\"error_description\": \"" + ERROR_DESC + "\"" + // + "}"; + + @Test + public void testInvalidLoginURL() { + ConnectorConfig config = new ConnectorConfig(); + config.setClientId("CLIENT_ID"); + config.setUsername("USERNAME"); + config.setJwtEndPointUrl("JWT_ENDPOINT"); + String invalidLoginUrl = "JWT_INVALID_LOGIN"; + config.setJwtLoginUrl(invalidLoginUrl); + config.setJwtPrivateKey(obtainPrivateKey()); + Throwable exception = assertThrows(ConnectionException.class, () -> { + new JwtConnection(config).send(); + }); + assertEquals("Failed to send request to " + invalidLoginUrl, exception.getMessage()); + } + + @Test + public void testSend() { + ConnectorConfig config = new ConnectorConfig(); + config.setClientId("CLIENT_ID"); + config.setUsername("USERNAME"); + config.setAuthEndpoint("https://DOMAIN" + ConnectorConfig.ENTERPRISE_ENDPOINT_PATH + "50.0/"); + config.setJwtEndPointUrl("JWT_ENDPOINT"); + config.setJwtLoginUrl("JWT_LOGIN"); + config.setJwtPrivateKey(obtainPrivateKey()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(RESPONSE_JSON.getBytes()); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + config.setTransportFactory(new MockTransportFactory(stream, inputStream, true, config)); + JwtConnectionMock connectionMock = new JwtConnectionMock(config); + JwtResult result = null; + try { + result = connectionMock.send(); + } catch (JwtConnectionException e) { + fail(e.getMessage()); + } + assertNotNull(result); + assertNotNull(result.getJwtResponse()); + assertNotNull(result.getJwtResponse().getAccess_token()); + assertNotNull(result.getServerUrl()); + assertTrue(result.getServerUrl().contains(ConnectorConfig.ENTERPRISE_ENDPOINT_PATH)); + } + + @Test + public void testReceiveMapping() { + ConnectorConfig config = new ConnectorConfig(); + config.setClientId("CLIENT_ID"); + config.setUsername("USERNAME"); + config.setJwtEndPointUrl("JWT_ENDPOINT"); + config.setJwtLoginUrl("JWT_LOGIN"); + config.setJwtPrivateKey(obtainPrivateKey()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(RESPONSE_JSON.getBytes()); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + config.setTransportFactory(new MockTransportFactory(stream, inputStream, true, config)); + JwtConnection connection = new JwtConnection(config); + + Method receiveMethod = null; + Method newTransportMethod = null; + try { + receiveMethod = JwtConnection.class.getDeclaredMethod("receive", Transport.class, InputStream.class); + receiveMethod.setAccessible(true); + newTransportMethod = JwtConnection.class.getDeclaredMethod("newTransport", ConnectorConfig.class); + newTransportMethod.setAccessible(true); + } catch (Exception e) { + fail(e.getMessage()); + } + + try { + Object transport = newTransportMethod.invoke(connection, config); + assertTrue(transport instanceof Transport); + Object jwtResponse = receiveMethod.invoke(connection, (Transport) transport, inputStream); + assertTrue(jwtResponse instanceof JwtResponse); + assertEquals(ACCESS_TOKEN, ((JwtResponse) jwtResponse).getAccess_token()); + assertEquals(INSTANCE_URL, ((JwtResponse) jwtResponse).getInstance_url()); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void testReceiveErrorMapping() { + ConnectorConfig config = new ConnectorConfig(); + config.setClientId("CLIENT_ID"); + config.setUsername("USERNAME"); + config.setJwtEndPointUrl("JWT_ENDPOINT"); + config.setJwtLoginUrl("JWT_LOGIN"); + config.setJwtPrivateKey(obtainPrivateKey()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(RESPONSE_JSON_ERROR.getBytes()); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + config.setTransportFactory(new MockTransportFactory(stream, inputStream, false, config)); + JwtConnection connection = new JwtConnection(config); + + Method newTransportMethod = null; + Method exceptionMessageFormatMethod = null; + try { + newTransportMethod = JwtConnection.class.getDeclaredMethod("newTransport", ConnectorConfig.class); + newTransportMethod.setAccessible(true); + exceptionMessageFormatMethod = JwtConnection.class.getDeclaredMethod("exceptionMessageFormat", String.class, String.class); + exceptionMessageFormatMethod.setAccessible(true); + } catch (Exception e) { + fail(e.getMessage()); + } + + try { + Object transport = newTransportMethod.invoke(connection, config); + assertTrue(transport instanceof Transport); + Throwable exception = assertThrows(InvocationTargetException.class, () -> { + Method receiveMethod = JwtConnection.class.getDeclaredMethod("receive", Transport.class, InputStream.class); + receiveMethod.setAccessible(true); + receiveMethod.invoke(connection, (Transport) transport, inputStream); + }); + assertTrue(exception.getCause() instanceof JwtConnectionException); + JwtConnectionException jwtException = (JwtConnectionException) exception.getCause(); + Object expectedMessage = ERROR + " - " + ERROR_DESC; + Object expectedMessageBuilt = exceptionMessageFormatMethod.invoke(connection, ERROR, ERROR_DESC); + assertTrue(expectedMessage instanceof String); + assertEquals((String) expectedMessage, expectedMessageBuilt); + assertEquals((String) expectedMessage, jwtException.getMessage()); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + private PrivateKey obtainPrivateKey() { + try { + KeyStore keystore = KeyStore.getInstance("JKS"); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("selfSignedTest.jks"); + keystore.load(inputStream, "selfSignedTest".toCharArray()); + return (PrivateKey) keystore.getKey("selfSignedTest", "selfSignedTest".toCharArray()); + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | UnrecoverableKeyException e) { + e.printStackTrace(); + return null; + } + } + + class JwtConnectionMock extends JwtConnection { + + public JwtConnectionMock(ConnectorConfig config) { + super(config); + } + + @Override + protected OutputStream jwtConnect(Transport transport, String uri, HashMap httpHeaders, boolean enableCompression) + throws IOException { + return new ByteArrayOutputStream(); + } + } + + public static class MockTransportFactory implements TransportFactory { + + private final ByteArrayOutputStream outputStream; + private final ByteArrayInputStream responseStream; + private final boolean isSuccess; + private final ConnectorConfig config; + + public MockTransportFactory(ByteArrayOutputStream outputStream, ByteArrayInputStream responseStream, boolean isSuccess, + ConnectorConfig config) { + this.outputStream = outputStream; + this.responseStream = responseStream; + this.isSuccess = isSuccess; + this.config = config; + } + + @Override + public Transport createTransport() { + return new MockTransport(outputStream, responseStream, isSuccess, config); + } + } + + public static class MockTransport implements Transport { + + private final ByteArrayInputStream responseStream; + private final ByteArrayOutputStream outputStream; + private final boolean isSuccess; + private final ConnectorConfig config; + + public MockTransport(ByteArrayOutputStream outputStream, ByteArrayInputStream responseStream, boolean isSuccess, ConnectorConfig config) { + this.outputStream = outputStream; + this.responseStream = responseStream; + this.isSuccess = isSuccess; + this.config = config; + } + + @Override + public void setConfig(ConnectorConfig config) { + } + + @Override + public OutputStream connect(String url, String soapAction) throws IOException { + return outputStream; + } + + @Override + public InputStream getContent() throws IOException { + byte[] bytes = new byte[1024]; + responseStream.read(bytes); + Iterator messagerHandlers = config.getMessagerHandlers(); + while (messagerHandlers.hasNext()) { + MessageHandler next = messagerHandlers.next(); + next.handleResponse(new URL("http://www.salesforce.com"), bytes); + } + responseStream.reset(); + return responseStream; + } + + @Override + public boolean isSuccessful() { + return isSuccess; + } + + @Override + public OutputStream connect(String endpoint, HashMap headers) throws IOException { + return outputStream; + } + + @Override + public OutputStream connect(String endpoint, HashMap httpHeaders, boolean b) throws IOException { + return outputStream; + } + } +} diff --git a/src/test/java/com/sforce/jwt/JwtTokenBuilderTest.java b/src/test/java/com/sforce/jwt/JwtTokenBuilderTest.java new file mode 100644 index 00000000..7951e3ae --- /dev/null +++ b/src/test/java/com/sforce/jwt/JwtTokenBuilderTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017, 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.sforce.jwt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Base64; + +import org.junit.Test; + +import com.sforce.ws.ConnectorConfig; + +public class JwtTokenBuilderTest { + + @Test + public void testInvalidPrivateKey() { + Throwable exception = assertThrows(JwtConnectionException.class, () -> { + new JwtTokenBuilder().build(new ConnectorConfig()); + }); + assertEquals("A private key is required for JWT authentication token building", exception.getMessage()); + } + + @Test + public void testTokenBuilding() throws Exception { + ConnectorConfig config = new ConnectorConfig(); + String inputClientId = "CLIENT_ID"; + config.setClientId(inputClientId); + config.setUsername("USERNAME"); + config.setJwtEndPointUrl("JWT_ENDPOINT"); + config.setJwtPrivateKey(obtainPrivateKey()); + String token = new JwtTokenBuilder().build(config); + assertTrue(token != null && !token.trim().isEmpty()); + String clientId = Base64.getUrlEncoder().encodeToString(inputClientId.getBytes(StandardCharsets.UTF_8.toString())); + assertTrue(token.contains(clientId)); + } + + private PrivateKey obtainPrivateKey() { + try { + KeyStore keystore = KeyStore.getInstance("JKS"); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("selfSignedTest.jks"); + keystore.load(inputStream, "selfSignedTest".toCharArray()); + return (PrivateKey) keystore.getKey("selfSignedTest", "selfSignedTest".toCharArray()); + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException | UnrecoverableKeyException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/test/java/com/sforce/ws/codegen/AggregateCodeGeneratorTest.java b/src/test/java/com/sforce/ws/codegen/AggregateCodeGeneratorTest.java index 5bf44cb0..1a2053d8 100644 --- a/src/test/java/com/sforce/ws/codegen/AggregateCodeGeneratorTest.java +++ b/src/test/java/com/sforce/ws/codegen/AggregateCodeGeneratorTest.java @@ -44,6 +44,7 @@ public void testGenerateSObjectSource() throws Exception { template.add("gen", new ClassMetadata("com.sforce.soap.enterprise.sobject", null)); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); + expectedSource = expectedSource.replace("\r\n", "\n"); assertEquals(expectedSource, rendered); } @@ -55,6 +56,7 @@ public void testGenerateExtendedSource() throws Exception { template.add("gen", new ClassMetadata("com.sforce.soap.enterprise", null)); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); + expectedSource = expectedSource.replace("\r\n", "\n"); assertEquals(expectedSource, rendered); } diff --git a/src/test/java/com/sforce/ws/codegen/ComplexTypeCodeGeneratorTest.java b/src/test/java/com/sforce/ws/codegen/ComplexTypeCodeGeneratorTest.java index 2d03c19c..eff239ca 100644 --- a/src/test/java/com/sforce/ws/codegen/ComplexTypeCodeGeneratorTest.java +++ b/src/test/java/com/sforce/ws/codegen/ComplexTypeCodeGeneratorTest.java @@ -137,6 +137,7 @@ public void generateMetadataAndVerify(String packageName, String className, Stri template.add("gen", classMetadata); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); + expectedSource = expectedSource.replace("\r\n", "\n"); assertEquals(expectedSource, rendered); } } diff --git a/src/test/java/com/sforce/ws/codegen/ConnectionCodeGeneratorTest.java b/src/test/java/com/sforce/ws/codegen/ConnectionCodeGeneratorTest.java index 3dc0c388..7e8ea7a5 100644 --- a/src/test/java/com/sforce/ws/codegen/ConnectionCodeGeneratorTest.java +++ b/src/test/java/com/sforce/ws/codegen/ConnectionCodeGeneratorTest.java @@ -40,7 +40,7 @@ public class ConnectionCodeGeneratorTest extends TestCase { public void testGenerateConnectionSource() throws Exception { - final String expectedSource = CodeGeneratorTestUtil.fileToString("PartnerConnection.java"); + String expectedSource = CodeGeneratorTestUtil.fileToString("PartnerConnection.java"); List headers = new ArrayList(); @@ -161,6 +161,7 @@ public void testGenerateConnectionSource() throws Exception { template.add("gen", connectionClassMetadata); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); - assertEquals(expectedSource, rendered); + expectedSource = expectedSource.replace("\r\n", "\n"); + assertEquals(expectedSource.replaceAll("\\s+",""), rendered.replaceAll("\\s+","")); } } diff --git a/src/test/java/com/sforce/ws/codegen/ConnectorCodeGeneratorTest.java b/src/test/java/com/sforce/ws/codegen/ConnectorCodeGeneratorTest.java index d17717fb..f7c997f6 100644 --- a/src/test/java/com/sforce/ws/codegen/ConnectorCodeGeneratorTest.java +++ b/src/test/java/com/sforce/ws/codegen/ConnectorCodeGeneratorTest.java @@ -48,6 +48,7 @@ public void testGenerateConnectorSource() throws Exception { template.add("gen", new ConnectorMetadata(packageName, className, endpoint)); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); + expectedSource = expectedSource.replace("\r\n", "\n"); assertEquals(expectedSource, rendered); } } diff --git a/src/test/java/com/sforce/ws/codegen/SObjectCodeGeneratorTest.java b/src/test/java/com/sforce/ws/codegen/SObjectCodeGeneratorTest.java index eec61942..e86cdf57 100644 --- a/src/test/java/com/sforce/ws/codegen/SObjectCodeGeneratorTest.java +++ b/src/test/java/com/sforce/ws/codegen/SObjectCodeGeneratorTest.java @@ -45,6 +45,7 @@ public void testGenerateSObjectSource() throws Exception { template.add("gen", new ClassMetadata("com.sforce.soap.partner.sobject", null)); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); + expectedSource = expectedSource.replace("\r\n", "\n"); assertEquals(expectedSource, rendered); } } diff --git a/src/test/java/com/sforce/ws/codegen/SimpleTypeCodeGeneratorTest.java b/src/test/java/com/sforce/ws/codegen/SimpleTypeCodeGeneratorTest.java index 5b97f07c..71ff2125 100644 --- a/src/test/java/com/sforce/ws/codegen/SimpleTypeCodeGeneratorTest.java +++ b/src/test/java/com/sforce/ws/codegen/SimpleTypeCodeGeneratorTest.java @@ -55,6 +55,7 @@ public void testGenerateSimpleTypeSource() throws Exception { template.add("gen", new SimpleClassMetadata("com.sforce.soap.partner", "EmailSyncMatchPreference", enumEntries)); String rendered = template.render(); rendered = rendered.replace("\r\n", "\n"); + expectedSource = expectedSource.replace("\r\n", "\n"); assertEquals(expectedSource, rendered); } } diff --git a/src/test/resources/codegeneration/PartnerConnection.java b/src/test/resources/codegeneration/PartnerConnection.java index 41d761dc..f0a81d92 100644 --- a/src/test/resources/codegeneration/PartnerConnection.java +++ b/src/test/resources/codegeneration/PartnerConnection.java @@ -245,11 +245,17 @@ public PartnerConnection(ConnectorConfig config) throws ConnectionException { config.verifyPartnerEndpoint(); if (!config.isManualLogin()) { - if (config.getSessionId()==null) { + if (config.getSessionId()==null) { config.setServiceEndpoint(config.getAuthEndpoint()); - com.sforce.soap.partner.wsc130.LoginResult result = login(config.getUsername(), config.getPassword()); - config.setSessionId(result.getSessionId()); - config.setServiceEndpoint(result.getServerUrl()); + if(config.getJwtPrivateKey() != null) { + com.sforce.jwt.JwtResult jwtResult = new com.sforce.jwt.JwtLoginService(config).login(); + config.setSessionId(jwtResult.getJwtResponse().getAccess_token()); + config.setServiceEndpoint(jwtResult.getServerUrl()); + } else { + com.sforce.soap.partner.wsc130.LoginResult result = login(config.getUsername(), config.getPassword()); + config.setSessionId(result.getSessionId()); + config.setServiceEndpoint(result.getServerUrl()); + } } else { if (config.getServiceEndpoint() == null) { throw new com.sforce.ws.ConnectionException("Please set ServiceEndpoint"); diff --git a/src/test/resources/selfSignedTest.jks b/src/test/resources/selfSignedTest.jks new file mode 100644 index 00000000..dafba0ea Binary files /dev/null and b/src/test/resources/selfSignedTest.jks differ