Why did this post get written – During my security assessments I perform on customer’s Salesforce environments, the most common critical issue I see is integrations that are running with System Administrator permissions.
This is very bad for security as the tool on the other end has complete control over your org. It can delete, corrupt and/or export ALL your data, deactivate ALL your users and effectively lock you out. If you use standard SOAP (or REST) with Username/Password, you have to give an external entity credentials which could be misused. Using the Web Browser OAuth flow for standard Connected Apps and REST is also dangerous because if the user is a System Administrator, then the external app again has full permissions.
This post is about what I consider to be best practice when it comes to integration with Salesforce using Java. The reasons I consider this to be best practice is that you are not sharing a password with the external party, you have granular control over what they getting access to and it uses one of Salesforce’s very low cost Integration User Licences.
I have spoken about the Integration User licence before and this is the exact use case for it – an external Java application logging into Salesforce to extract, create or update data.
To clear up the acronyms in the title – WSC is the Salesforce Web Services Connector, JWT is a JSON Web Token – a standards-based approach to sharing authentication between a client and a server, and Connected Apps are Salesforce’s approach to controlling access to Salesforce by external applications – this supports JWT as an authentication method.
As an aside, I am not a modern programmer – I do not use IDEs as I find they hide a lot of simple things behind complicated configuration. You will find I use batch files (which can easily map to .sh files in *nix) for compilation and running the code. This is easier for me to understand and I hope you also. If you use an IDE, it will also hopefully make moving my code into the IDE easier.
We are going to go through the four main steps I take to get the application running and within those main steps, all the individual steps to get it to work. Explanations along the way will, I hope, assist with making it clear. I welcome feedback on the content and explanations.
For the entire demo, I am using the C:\TMP\WSC and JWT and Connected App directory as the top directory.
If you do not have a Salesforce developer org to play with, you can sign up for one at https://developer.salesforce.com/signup. Use something like your email address followed by .jwtdemo for the username as these need to be unique across all of Salesforce. For myself, I used doug@platinum7.com.au.jwtdemo. You can have many users in many orgs with your email address; however, the username for each needs to be globally unique…
Step 0 – Get an appropriate JDK
I am assuming that since you are looking at this you should have a JDK that works for you. If not, I am using the Azul JDK version 21 LTS which is open source and does not need any commercial licencing. https://www.azul.com/downloads/#zulu
For Windows, download the ZIP file into JDK under the top directory and then uncompress it into the same directory, then rename the directory jdk21.0.2 to get this:
You can then delete the ZIP file if you wish.
Step 1 – Generate the WSC jar file
This is the Salesforce Web Services Connector which makes all the hard work of using the Salesforce API go away. The source code is hosted at https://github.com/forcedotcom/wsc and is updated regularly.
To get the package prebuilt, go to https://mvnrepository.com/artifact/com.force.api/force-wsc and click on the most recent version. Create a directory for the WSC under the top directory, I tend to version it, so I have WSC, then I have 60.1.1 (in this case) as a directory inside – all the following files need to be stored in there.
From there click on the View All to allow you to see all the files in the repository.
We need the force-wsc-XXXXX-uber.jar and force-wsc-XXXXX.jar files.
Now, go back one page and get all the Compile and Provided dependencies jar file.
After all this, you should have a directory with these files (in the version you downloaded).
Now, we need to get the Salesforce Partner WSDL file. To do this, go to Setup area in your org, search for API by using the Quick Find box in the top left. Click the Partner WSDL link and save this file as PartnerXX.wsdl, where XX is the current version number, into the same directory. The version number is near the top of the wsdl file.
Now it is time to create the compile script. Use your favourite editor to create _compile.bat, the content of mine is below. Yours may be different depending on the versions you are using.
set PATH=..\..\JDK\jdk21.0.2\bin;%PATH%
java -cp . -jar force-wsc-60.1.1-uber.jar Partner60.wsdl partner60.jar
pause
Now run it and you should get something like this:
Now, you have generated the Java classes we will be calling from our code from the WSDL file, and these are now in the partner60.jar. You can leave it as it is; however, there are a lot of .java files in the partner60.jar file which take up space and are not needed at run time. Keep a copy of the original partner60.jar file as you can learn how things work by looking at the Java files. I use WinRAR which will natively read JAR files and allow me to delete the .java files. The open source 7Zip application also does this, however you cannot sort by extension so it takes a lot longer to choose each .java file to delete.
Step 2 – Simple Application using the WSC
This application is going to query the Account object using username and password. I am only doing this to make sure it works and to demo some simple code.
// Needed for WSC
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.soap.partner.QueryResult;
// Needed for code
import java.util.Scanner;
public class QueryAccount
{
static PartnerConnection connection;
public static void main (String [] args)
{
QueryResult queryResults;
int numResults;
// Get the input from the user
Scanner inScanner = new Scanner (System.in);
System.out.print ("Username: ");
String username = inScanner.nextLine ().trim ();
System.out.print ("Password: ");
String password = inScanner.nextLine ().trim ();
System.out.println ("\n");
// Connect to Salesforce using username and password
// The version in the URL below should be the same as the
// WSC major version you used which is the same as the
// partner.wsdl you used...
ConnectorConfig config = new ConnectorConfig ();
config.setUsername (username);
config.setPassword (password);
config.setAuthEndpoint ("https://login.salesforce.com/services/Soap/u/60.0");
config.setCompression (true);
try
{
connection = Connector.newConnection (config);
}
catch (ConnectionException ce)
{
System.err.println ("Could not connect to Salesforce.");
System.err.print ("Please make sure the username and");
System.err.println (" password are correct.\n");
System.err.println (ce);
ce.printStackTrace ();
System.exit (1);
}
try
{
System.out.println ("Querying for all Accounts");
// Get the fields that I want
queryResults = connection.query ("SELECT Id, Name, BillingState FROM Account");
numResults = queryResults.getSize ();
System.out.println ("There are " + numResults + " Account records\n");
for (SObject acct: queryResults.getRecords ())
{
System.out.print ("Id: " + acct.getField ("Id").toString () + " ");
System.out.println ("Name: " + acct.getField ("Name").toString ());
Object fieldValue = acct.getField ("BillingState");
System.out.println ("BillingState: " +
(fieldValue == null ?
"NULL" : fieldValue.toString ()) + "\n");
}
}
catch (Exception e)
{
System.err.println ("Could not query Salesforce.");
System.err.println (e);
e.printStackTrace ();
System.exit (1);
}
System.out.println ("\nFinished.");
} // main
} // QueryAccount
Create an Application directory under the top level directory. Put the above code into a file into the Application directory called QueryAccount.java. Now copy the force-wsc-60.1.1.jar and partner60.jar from the WSC\60.1.1 directory to the Application directory as well. These are all that are required to run the Java code – the other .jar files are not needed as they were just for creating the partner60.jar file.
Now we need to create the _compile.bat and _run.bat script files in the Application directory. They are as below:
This is the _compile.bat file:
set PATH=..\JDK\jdk21.0.2\bin;%PATH%
javac -classpath ./partner60.jar;./force-wsc-60.1.1.jar;. QueryAccount.java
pause
This is the _run.bat file:
@echo off
cls
set PATH=..\JDK\jdk21.0.2\bin;%PATH%
java -classpath ./partner60.jar;./force-wsc-60.1.1.jar;. QueryAccount
pause
Now let’s compile and then run the code. Just double click on the _compile.bat file in the Application directory. When it has compiled successfully, double click on the _run.bat file and enter your details. It should end up like the picture below. You may need to use your Security Token appended to your password if the connection fails.
See https://help.salesforce.com/s/articleView?id=sf.user_security_token.htm&type=5 for information on how to reset your Security Token. (If you are not using a System Administrator account [you are if you created a developer org], you may get a runtime error about permissions.)
So now we have proven that the WSC library is working and we can run code to connect to Salesforce. Onto the next steps – setting up the JWT, Connected App and then modifying the code to suit…
Step 3 – Connected App JWT setup
We need to create the certificate for the Connected App, then create the Connected App in Salesforce and then create the Integration user and add their permissions. This looks like a lot of work; however, it is not that tricky when you have done it a few times.
The following instructions on creating the certificate came from https://www.apexhours.com/salesforce-oauth-2-0-jwt-bearer-flow/; however, the instructions were not totally correct – my version below works.
We will use OpenSSL to generate the keys, etc. For Windows, I used this https://slproweb.com/products/Win32OpenSSL.html version. Please download the Win64 OpenSSL v3.2.1 Light (or later) version and install it. For *nix, look for the most recent OpenSSL version for your distribution.
Go to the Start Menu in Windows and select Win64 OpenSSL Command Prompt, then change directory to C:\TMP\WSC and JWT and Connected App and create the Connected App directory where we will be doing all the work and then change directory into that directory.
Generate an RSA private key by typing this command. The x below is the password that will be for the server.pass.key file. You can leave it as x if you like, however you may want to use a password without spaces. We do not need to share this private key; we share a certificate based on this key.
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
Create a key file from the server.pass.key file. The server.key is our RSA private key. Again, the x is the password from the previous step. If you changed it, please replace x with your password.
openssl rsa -passin pass:x -in server.pass.key -out server.key
Create a request for the generation of the certificate needed for the JWT. This command will ask a number of questions, I used these answers, however you should use answers appropriate to yourself. If you do not have an Organisation Name, you can use Self.
Country: AU
State or Province: Victoria
Locality Name: Melbourne
Organisation Name: Platinum7
Organisational Unit Name: <blank>
Common Name: <blank>
Email Address: doug@platinum7.com.au
For the challenge password and company name I left them blank.
openssl req -new -key server.key -out server.csr
Now generate the certificate using the request from the previous step. The number of days validity does matter, so you will have to refresh the keystore for the code and the server certificate in the Connected App when this expires.
openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
We now need to create a Java Key Store as this is the format that Java needs for importing keys into our code. Copy the server.key as server.pem.
copy server.key server.pem
And now create the key store in pkcs12 format. You will need to enter a password – please remember it!
openssl pkcs12 -export -in server.crt -inkey server.pem -out keystore.p12
Now it is time to use the Java Keytool program to create the Java Key Store for your Java code. This command needs to be entered all on one line. Enter a password for the destination keystore and the password you created in the previous step for the source keystore. The destination keystore password will be used in your Java code to extract the private key from the keystore.
..\JDK\jdk21.0.2\bin\keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12
-destkeystore servercert.jks -deststoretype pkcs12
We now have the information to allow Salesforce to use JWT flows for authentication. Now it is time to create the connected application and the user with permissions that will be used to connect…
Login into your org as a System Administrator and go to Setup.
Go to the App Manager.
In the top right, click on New Connected App.
Enter Query Accounts as the Connected App Name.
Tab through to the Contact Email field and the API Name will be set automatically.
Enter your email address as the Contact Email.
Enter your phone number as the Contact Phone.
Choose a logo and icon URL from Salesforce’s library by clicking the Choose one of our sample logos link below the field to see what is there. I used https://login.salesforce.com/logos/Standard/custom/logo.svg and https://login.salesforce.com/logos/Standard/custom/icon.png. Or upload your choices.
Enter Sample JWT App in the Description field.
Click the Enable OAuth Settings checkbox to open the area below.
Enter https://localhost:9876/OauthRedirect into the Callback URL field (it is never used and can be any port number, not just 9876), however you will need to modify the code if you use a different value.
Click the Use digital signatures checkbox.
Click the Choose File button under the Use digital signatures checkbox and choose the server.crt file from the Connected App directory.
In the Selected OAuth Scopes area, add the following scopes.
Access the identity URL service (id, profile, email, address, phone)
Manage user data via APIs (api)
Perform requests at any time (refresh_token, offline_access)
Leave the rest of the settings as they are.
It should look like this image before you click Save.
Click Save if it looks correct, then click Continue.
We need to get the Consumer Key and the Consumer Secret for later, so click on the Manage Consumer Details button. You will get an MFA alert, so approve this.
Copy the Consumer Key and Consumer Secret into a text file, remembering which is which, and keep for later when we are making changes to the code to use the Connected App.
It is now time to create the user and add their permissions
Go to Permission Sets in Setup and create a new one with the label set to Allow Query Accounts then press tab to get to the API field and it will create the correct value. Leave everything else as is.
When it has been created, click on the Object Settings link in the Apps area as we need to allow access to the Account object and the appropriate fields.
Click on the Accounts link, click the Edit button then click on the checkbox beside View All, then scroll down and check the tickbox beside the BillingAddress field and click Save.
In the real world outside this demo, you will need to access different data and possibly, delete and update data. What this step does is limit the access to only the fields and objects chosen. If you need to update or delete data, then choose those options for the appropriate fields and objects. This is why this is best practice for security – you are using the principal of least privilege. This is exactly why you should not use System Administrator profile users for API access…
Create a new user to be used exclusively for the App (as you should do for best practice security) by going to Users in the Setup area and clicking the New button. Use your email address and a username like queryaccounts@yourcompany.com.jwtdemo and use the Salesforce Integration licence type and the Minimum Access - API Only Integrations profile, uncheck all the tickboxes in the General Information area (to minimise access) and then check the No MRU Updates tickbox to enhance performance.
For other apps, create other users and other permission sets with just the correct access rights and limit it to one user per Connected App – that way you are totally aware of what that user is doing to your data. If you have one API user for more than one app, then you are never sure which app changed which data...
Scroll down to the bottom and click Save. It will now create the user and return you to the User page.
On the User page, on the top middle of the page, hover over the Permission Set License Assignments and then click the Edit Assignments button on the pop up.
Enable the Salesforce API Integration permission set license and click Save.
Again, on the User page, on the top left, hover over the Permission Set Assignments and then click the Edit Assignments button on the pop up.
Add the Allow Query Accounts permission set to the Enabled Permission Sets and click Save.
You will have received an email with the Verify Account for the new user. If you use Gmail and Chrome, right click on the Verify Account button in the middle of the email and then choose Open Link in Incognito Window. If not, get the link from the button and paste it into an Incognito/Private/… window and hit Enter.
Fill in the user’s password and security question. You will not be using this password, however keep it safe.
Now we have the Java Key Store for the code, we have the User with the appropriate permissions, and the Connected App, it is now time to configure the Connected App to allow this new user to connect to it.
If you have API Access Control set up in your org, disable it temporarily so you are able to connect an OAuth App.
We are now going to limit access to the Connected App to just the user with the permission set we created. Go to the App Manager section of Setup and click on the dropdown arrow on the far right of the Query Accounts app and choose Manage.
Click on Edit Policies at the top of the page.
Change the Permitted Users to Admin approved users are pre-authorized and click OK on the warning popup and then click Save.
What this does is stop everyone in your org from using the app. It will just be the users with the appropriate permission sets and/or profiles listed in the related lists below. You should do this for all Connected Apps – especially “dangerous” ones like Salesforce’s Workbench or Dataloader.io, because without limiting access, anyone in your org can connect these to your org and export all the data they can see…
Scroll down the page and you will see a Permission Sets related list. Click the Manage Permission Sets button and make sure that there is a check mark against the Allow Query Accounts permission set and no other permission sets are checked. Click Save and we are done with the configuration.
If you disabled API Access Control, please re-enable it.
That is now it! Time to change the code to use the JWT flow. For more info on API Access Control and why it is important, please see my blog on https://doug-merrett.medium.com which will be released mid-April 2024.
Step 4 – Change the code to use JWT to connect
We are going to replace the authentication code in the initial Java application we wrote as we will be using JWT to do the connection, however lets discuss what a JWT looks like as we will be building one in the Java code – you may want to follow along in the code as we go through this. The new code is in a ZIP file referenced below.
There is a lot of detailed information on the format on jwt.io’s site: https://jwt.io/introduction.
In a nutshell, the JWT is made up of three parts separated by full stops; the header, the payload and the signature. These are JSON data structures.
The header we need is just the algorithm used: {"alg":"RS256"} as we do not need the type.
The payload we need is a registered claims type with these claims: iss (issuer), sub (subject), aud (audience), and exp (expiration time). The issuer is the Consumer Key from the Connected App, the subject is the username of our user, the audience is the login URL (either https://login.salesforce.com for production, https://test.salesforce.com for sandboxes or https://XXXX.my.salesforce.com if you have blocked API access to using the standard login URLs), and the expiration time is now plus two minutes. The format of the expiration time is the Unix Timestamp format which is the number of seconds since January 1st 1970.
The signature is how the receiver knows that this message has not been compromised. It uses this algorithm to generate a cryptographical signature:
HMACSHA256 (base64UrlEncode (header) + "." + base64UrlEncode (payload), PrivateKey)
We will need to use the Java security libraries to provide the HMACSHA256 function and the ability to use keys from the Java keystore. The Base64 functions will be provided by an Apache library.
Now we have the JWT, we need to login to Salesforce by HTTPS POST-ing the JWT, the Consumer Key and the Consumer Secret from the Connected App to the /services/oauth2/token endpoint of the login URL we are going to use. If the planets align and we haven’t made a mistake, then we will receive a JSON response which will contain the Session ID and the Identity URL. The Session ID is used as the authentication token for further access and the Identity URL is used to get the Partner URL for the WSC framework to use when querying the Salesforce API.
We get the Partner URL from Salesforce by doing an HTTPS GET of the Identity URL passing the API version number and the Session ID. This returns a JSON payload with all the URLs and we just grab the Partner one.
After all that, we now have authenticated using the JWT, received the Session ID from the oauth2 endpoint and have the Partner URL, we can replace the login code from before with these values. See below for the before and after code:
// Connect to Salesforce [Username/Password Code]
ConnectorConfig config = new ConnectorConfig ();
config.setUsername (username);
config.setPassword (password);
config.setAuthEndpoint ("https://login.salesforce.com/services/Soap/u/60.0");
config.setCompression (true);
// Connect to Salesforce [JWT Code]
ConnectorConfig config = new ConnectorConfig ();
config.setSessionId (sessionId);
config.setCompression (true);
config.setServiceEndpoint (partnerURL);
The code after that is the same.
We need to get a JSON library and the Base64 encoding library to make all this work. The JSON library is available here (https://mvnrepository.com/artifact/org.json/json) and the Base64 library is here (https://mvnrepository.com/artifact/commons-codec/commons-codec). Get the most recent JAR file of these in the same we did earlier for the WSC dependencies. For those curious as to why we are not using the inbuilt Java Base64 library, it is because it pads the Base64 data whereas the Apache commons does not and JWT needs unpadded data.
Put these files in the Application directory under the top-level directory we used before and overwrite the previous code, _compile.bat and _run.bat files with the contents of this ZIP file: https://links.platinum7.com.au/JWTCode. You may need to modify the _compile.bat and _run.bat files as you may have newer library JAR files. You will need to modify the code to have the appropriate username, Java Keystore password, and the Consumer Key and Consumer Secret from the Connected App you created earlier. Copy the servercert.jks file from the Connected App directory to the Application directory as the code needs it to be in the same directory as the Java app. Compile and run and you get the same as before, except with a better approach to security.
Please let me know if you have any questions or comments.
Doug Merrett – doug@platinum7.com.au
댓글