Let’s say you have Salesforce org which manages Device data. Also, you have Java Spring Boot application and you want Devices data from Salesforce to be securely synchronized to Java application. To achieve this we will use OAuth 2.0 authorization framework with client_credentials flow which is ideal for Server-to-Server communication without user involved. So, let get started.
In this tutorial I’m going to use Java application and Salesforce org from previous tutorial about integration from Java Spring Boot app to Salesforce org. In that tutorial we have implemented synchronization of Device records from Java Spring Boot application to Salesforce org. In this tutorial we will do vice versa: from Salesforce to Java. GitHub reference: https://github.com/volodymyrbervetskyy/sf_java_integration. If you prefer videos over articles, you can check for the same tutorial in video format on my YouTube channel: https://www.youtube.com/watch?v=Qjni5xNtplU
1. Setup Java Spring authorization server
In order to login to Java application via OAuth we need to setup authorization server. First of all we need to add dependency “spring-boot-starter-oauth2-authorization-server”
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
...
In SecurityConfig we need to add SecurityFilterChain Bean for setting up authorization server configurations
...
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.build();
}
...
applyDefaultSecurity(http) method adds filters necessary for handling OAuth2-related requests and configures the default endpoints required for an OAuth2 authorization server like: /oauth2/token endpoint, /oauth2/introspection endpoint etc.
2. Register Salesforce OAuth client in Java
Let’s register Salesforce OAuth client via which we will be communicating from Salesforce to Java application.
...
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient sfClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(JAVA_OAUTH_CLIENT_ID)
.clientSecret( passwordEncoder().encode(JAVA_OAUTH_CLIENT_SECRET) )
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("DEVICE_ADMIN")
.tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
.build();
return new InMemoryRegisteredClientRepository(sfClient);
}
...
We are creating an instance of RegisteredClient, providing it clientId and clientSecret. Setting ClientAuthenticationMethod as CLIENT_SECRET_BASIC which means that during token request we need to pass Base64 value of CLIENT_ID and CLIENT_SECRET in Authorization header. Specifying that the client will be working only via client_credentials flow. Providing “DEVICE_ADMIN” scope. Indicating that the token will be a opaque token (OAuth2TokenFormat.REFERENCE) and will require validation against the authorization server.
3. Setup Java Spring resource server
To setup Java OAuth Resource Server, firstly, we need to add “spring-boot-starter-oauth2-resource-server” dependency
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
...
Then we can define resourceServerSecurityFilterChain
...
@Bean
@Order(2)
public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/device/**").hasAuthority("SCOPE_DEVICE_ADMIN")
.anyRequest().denyAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaqueToken -> opaqueToken
.introspectionUri(BASE_URL + "/oauth2/introspect")
.introspectionClientCredentials(JAVA_OAUTH_CLIENT_ID, JAVA_OAUTH_CLIENT_SECRET)
)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable())
;
return http.build();
}
...
Our Resource Server will be processing requests only to “/api/**” paths. Also to access “/api/device/**” paths client need to have “DEVICE_ADMIN” scope. For all other paths access is denied as of now. Then we are enabling OAuth Resource Server support and specifyng that opaque tokens will be used for token validation. Also we need to specify introspection url and client credentials, so that Resource server could validate the token. Disabling sessions to make every incoming request self-contained and independently authenticated. Also, we need to disable CSRF protection since Resource Server authentication is not session-based.
4. Create Java REST service
Let’s go to DeviceService class and define here method to save Devices data sent from Salesforce
...
public List<Device> saveDevices(String devicesData) {
List<Map<String, Object>> listOfDevicesData = null;
try {
listOfDevicesData = new ObjectMapper().readValue(devicesData, new TypeReference<List<Map<String, Object>>>(){});
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
List<Device> devices = new ArrayList<>();
for(Map<String, Object> deviceDataMap : listOfDevicesData){
Device d = new Device();
String strId = (String) deviceDataMap.get("id");
if(strId != null && !strId.isBlank()){
d.setId( Long.parseLong(strId) );
}
d.setSfId( (String) deviceDataMap.get("sfId") );
d.setManufacturer( (String) deviceDataMap.get("manufacturer") );
d.setModel( (String) deviceDataMap.get("model") );
d.setPrice( (Double) deviceDataMap.get("price") );
devices.add(d);
}
return deviceRepository.saveAll(devices);
}
...
As a parameter we have an array of Device data in JSON format. We are deserializing it and transforming to Device objects. At the end we are committing changes to the database and returning the saved objects.
To handle request from Salesforce let’s create REST ApiController
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private DeviceService deviceService;
@PostMapping("/device/saveDevices")
public List<Device> saveDevices(@RequestBody String devicesData) {
return deviceService.saveDevices(devicesData);
}
}
This controller will be processing requests made to “/api/**” paths. Method “saveDevices” is responsible for taking in Device data sent from Salesforce and passing this data to DeviceService. Also we are returning saved Devices data as a response back to Salesforce to be able to populate external ids.
5. Deploy Java Spring Boot app to remote server
Let’s create .jar file for our Java Spring Boot application. Go to ‘Maven’ menu and run ‘package’ command
It will package the compiled code of our project into a distributable format, that is .jar file. In target folder we can see that our .jar file is ready.
Now we can upload our .jar file to remote server. I will use alwaysdata.com hosting. It provides 100 Mb free plan with database. If you also would like to continue with alwaysdata.com hosting, you can follow the steps in my video tutorial: Integration between Salesforce and Java Spring Boot via OAuth 2.0 client credentials flow.
6. Setup Named Credentials and External Credentials in Salesforce
To be able to make callouts from Salesforce we need to create External Credentials and Named Credentials. In setup, search for “Named Credentials”. Switch to “External Credentials” tab. Press “New” button
Populate “Label” and “Name”. Select “Authentication Protocol” as “OAuth 2.0”. As “Authentication Flow Type” select “Client Credentials with Client Secret Flow”. In “Identity Provider URL” field you should provide token URL. Add “DEVICE_ADMIN” scope. Press “Save” button.
Create new Principal
Provide a name. ClientId and ClientSecret we should take from settings of our Java application. They are stored in environment variables. Copy paste these values and press “Save”.
Go back to Named Credentials. Create new Named Credential
Populate “Label” and “Name”. Provide base URL to java Resource Server. Select DeviceManagerExternalCredential. Press “Save”.
In order to provide users with ability to make callouts, we need to give them access to External Credential Principal. For this purpose, let’s create a Permission Set called “DeviceManagerSync”
Go to “External Credential Principal Access”. Select “DeviceManagerAdminPrincipal” and press “Save”. Assign this Permission Set to needed users.
7. Make callout to synchronize Device data from Salesforce to Java application
On Salesforce side let’s create “DeviceDTO” class that will contain data to be used in callouts.
public class DeviceDTO {
public String id;
public String sfId;
public String manufacturer;
public String model;
public Decimal price;
public DeviceDTO(Device__c device) {
this.id = device.ExternalDeviceManagerAppId__c;
this.sfId = device.Id;
this.manufacturer = device.Manufacturer__c;
this.model = device.Name;
this.price = device.Price__c;
}
}
It has constructor, with Device record in parameter, so that populates we could conveniently setup Devcie data.
Also we need to create “DeviceSyncService” class that will contain all the needed service logic
public with sharing class DeviceSyncService {
public static Boolean isItDmJavaAppIntegrationContext = false;
public static void checkIfItIsDmJavaAppIntegrationContext(List<Device__c> newDevices){
isItDmJavaAppIntegrationContext = newDevices[0].LastlyModifiedByDMJavaApp__c == true;
if(isItDmJavaAppIntegrationContext){
for(Device__c device: newDevices){
device.LastlyModifiedByDMJavaApp__c = false;
}
}
}
public static List<DeviceDTO> convertDevicesToDTOs(List<Device__c> devices){
List<DeviceDTO> dtos = new List<DeviceDTO>();
for(Device__c device: devices){
dtos.add(new DeviceDTO(device));
}
return dtos;
}
public static void updateExternalDMAppId(List<DeviceDTO> resDeviceDtos, Map<Id, Device__c> devicesMap){
List<Device__c> devicesToUpdate = new List<Device__c>();
for(DeviceDTO deviceDto: resDeviceDtos){
Device__c device = devicesMap.get(deviceDto.sfId);
if(device.ExternalDeviceManagerAppId__c != deviceDto.id){
devicesToUpdate.add(
new Device__c(
Id = deviceDto.sfId,
ExternalDeviceManagerAppId__c = deviceDto.id
)
);
}
}
update devicesToUpdate;
}
public static List<Device__c> filterDevicesToSync(List<Device__c> newDevices, Map<Id, Device__c> oldDevicesMap){
if(oldDevicesMap == null || oldDevicesMap.isEmpty()){
return newDevices;
}
List<Device__c> devicesToSync = new List<Device__c>();
for(Device__c newDevice: newDevices){
Device__c oldDevice = oldDevicesMap.get(newDevice.Id);
if(newDevice.Name != oldDevice.Name
|| newDevice.Manufacturer__c != oldDevice.Manufacturer__c
|| newDevice.Price__c != oldDevice.Price__c)
{
devicesToSync.add(newDevice);
}
}
return devicesToSync;
}
public static void syncDevicesToExternalDMApp(List<Device__c> devicesToSync){
if(devicesToSync != null && !devicesToSync.isEmpty()){
DeviceSyncQueueable dsq = new DeviceSyncQueueable(
new Map<Id, Device__c>(devicesToSync).keySet()
);
System.enqueueJob(dsq);
}
}
}
Method “convertDevicesToDTOs” transforms list of Device records to list of Device DTOs. Method “updateExternalDMAppId” processes response sent back from Java and updates external id field on Device records. Method “syncDevicesToExternalDMApp” takes in list of Devices from trigger, extracts Device ids and pass these ids to Queueable job and enqueues it.
Let’s imagine that in the future on Salesforce side new fields will be added to Device object which should not be synchronized to Java application. If values of only these fields will get updated – we don’t need to make callouts to Salesforce since it has no sense. The data that is getting synchronized is not changed. For this reason, we have created a filter method “filterDevicesToSync”. It takes a list of new Device records and map of old Device records. If old map is null or empty – that means that this is insert operation, in such a case method returns all the Device records. Otherwise, filter verifies whether any of the fields that should be synchronized is changed and if so – adds it to the “devicesToSync” collection which is returned in the end.
Let me also explain why do we need “isItDmJavaAppIntegrationContext” static property and “checkIfItIsDmJavaAppIntegrationContext” method: since we have two-way integration, when Java sends updates to Salesforce, DeviceTrigger will be excuted and logic to make callout to Java app also will be executed, this is a redundant callout. To avoid it, we will utilize indicator field called “LastlyModifiedByDMJavaApp__c” which is always set to “TRUE” value when callout is made from Java side. So, static indicator variable called “isItDmJavaAppIntegrationContext” is set it to “FALSE” value by default. In method “checkIfItIsDmJavaAppIntegrationContext” we are vefying if update is done from Java side and respectively populating our static indicator. If it is Java side update – we should not synchronize Device records to Java and need to reset the value of “LastlyModifiedByDMJavaApp__c” field.
Since in Trigger transaction we cannot make callouts, we will utilize Queueable for this purpose. I prefer Queueable over Fututre method, since Queueable is more flexibe. It allows passing complex objects (like SObjects or collections) to Queueable job, chaining Queueable jobs and also it supports Finalizer which is crucial for debugging failed Queueable jobs.
So, let`s create Queueable class called “DeviceSyncQueueable”
public with sharing class DeviceSyncQueueable implements Queueable, Database.AllowsCallouts {
private Set<Id> deviceIds;
public DeviceSyncQueueable(Set<Id> deviceIds){
this.deviceIds = deviceIds;
}
public void execute(QueueableContext context) {
List<Device__c> devicesToSync = [
SELECT Name, ExternalDeviceManagerAppId__c, Manufacturer__c, Price__c
FROM Device__c
WHERE Id IN :deviceIds
];
String requestBody = JSON.serialize(
DeviceSyncService.convertDevicesToDTOs(devicesToSync)
);
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:DeviceManagerNamedCredential/api/device/saveDevices');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(requestBody);
HTTPResponse res = new Http().send(req);
String body = res.getBody();
DeviceSyncService.updateExternalDMAppId(
(List<DeviceDTO>) JSON.deserialize(body, List<DeviceDTO>.class),
new Map<Id, Device__c>(devicesToSync)
);
}
}
To be able to perform callouts we need to implement Database.AllowsCallouts interface. The constructor takes in set of ids of Device records that should be sent to Java application. Then we are querying required data. Preparing request body. Creating an instance of HttpRequest with specifying Named Credential and external url to be called. Executing request, retrieving body. Calling “DeviceSyncService” to update external Id field.
Now we can create Device trigger to call Queueable job. The trigger will be executed on after insert and on after update
trigger DeviceTrigger on Device__c (before insert, before update, after insert, after update) {
if(Trigger.isBefore){
if(Trigger.isInsert || Trigger.isUpdate){
DeviceSyncService.checkIfItIsDmJavaAppIntegrationContext(Trigger.new);
}
} else {
if(Trigger.isInsert || Trigger.isUpdate){
if(!DeviceSyncService.isItDmJavaAppIntegrationContext ){
DeviceSyncService.syncDevicesToExternalDMApp(
DeviceSyncService.filterDevicesToSync(Trigger.new, Trigger.oldMap)
);
}
}
}
}
8. Demo
Here is the demo video that shows the results we’ve achieved:
Leave a Reply