Archive

Archive for the ‘Uncategorized’ Category

Salesforce Canvas and Spring Boot App.

July 24, 2020 Leave a comment

Salesforce Lightning provides 2 ways to embed an external application in Salesforce. One could use a Canvas App, or, embed an iFrame in the Lightning Component. When using Canvas, there are a couple of ways the users could be authenticated in order to use the sdk (admin approved and self authorized users). The documentation is interleaved and did not provide a clear path of implementation. In this blog here, I have tried to cover one way to use Spring Boot and use a signed request from Canvas for the authentication.

The blog here creates a simple Spring Boot application with ssl enabled and Thymeleaf as the template. The focus will be on getting this application in the Canvas app and working our way through the signed request authentication (Thymeleaf could be replaced by other solutions. Similarly, Spring boot could be replaced by other frameworks if desired)

Here are the steps we will take to set this up.

  1. Create a Canvas App in Salesforce
  2. Create a basic Spring boot app sends a basic template back. (This will mimic a form but will not have the form wired to the application)
  3. Adding the application to the Canvas on a Lightning Component and page
  4. Refreshing a signed request.
  5. Decoding and verifying a signed request.
  6. Looking at the Canvas Request object
  7. Creating an event from the Application
  8. Consuming the event on the Lightning Component
  1. Creating the Canvas App in Salesforce
    Go to Setup | Apps | App Manager in Salesforce and click on the “New Connected App” button on the top right of the page.
    In the “New Connected App” page fill the following
    Connected App Name: My Spring Boot Canvas App
    API Name : (Will be filled automatically) My_Spring_Boot_Canvas_App
    Contact Email: Your-email-here
    Click on “Enable OAuth Settings”, add the callback URL as http://localhost:8080 and provide “Full Access (full)” in the “Available OAuth Scopes” (Note: In the production environment, you might want to restrict the privileges here)
    Skip to “Canvas App Settings” and check the checkbox “Canvas”
    Fill the following fields that show up in the “Canvas App Settings” section
    Canvas App Url: http://localhost:8080
    Access Method: Signed Request(POST)
    Locations: “Lightning Component” (We will put the Canvas app in the Lightning Component for now)

Once the App is created, locate your App in the App Manager and select “Manage” on the App. This will take you to a screen where you can provide your user permissions to access this App. In this page, go down to “Profiles” section, click on “Manage Profiles” and add your users Profile to the list. (Note: Permission Sets could be used here as an alternative)

  1. Creating a Spring Starter Project with Thymeleaf.
    In my example I use the Spring Starter from Spring STS IDE to create a base project for Spring Boot. The same could be done using Spring Initializr (https://start.spring.io/)
    We will create a Controller class as follows. This will have a request with method type as post and will accept a request parameter. This request parameter will have the signed request from Salesforce when the Canvas App loads.
package com.wordpress.codesilo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {

	@RequestMapping(value = "/", method = RequestMethod.POST)
	public String index(@RequestBody String signedRequest) {
		System.out.println(signedRequest);
		return "index";
	}
	
}

For now, we will send a simple index page back – using thymeleaf template. For this the index.html will be in the “templates” folder in the “resources” of the project. Here is what our index.html page looks like. We will change this later.

<html>
<body>
	<h1>Main Index Page</h1>
	<p> Canvas Page Content</p>
	
	<input type="text"/>
	<button id="ctxlink" >Some button</button>
</body>
</html>

At this point if we start the server and call the following endpoint http://localhost:8080/ from the browser, we should get a method not supported error page. (405 status). But if we use Postman to call the same endpoint but as a “POST” request with some dummy body payload, we should get back the index.html content in the response.

  1. Adding the application to the Canvas on a Lightning Component and page
    Create a Lightning Component with the name “SpringBootCanvasApp” and add this on the cmp file.
<aura:component implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome">
	<lightning:card>
    <force:canvasApp developerName="My_Spring_Boot_Canvas_App " maxHeight="200" maxWidth="200"/>
        </lightning:card>
</aura:component>

This creates the Lightning Component will we will place in a Lightning page next. This component contains the CanvasApp (My_Spring_Boot_Canvas_App) that points to the Spring Boot app we created earlier.

Go to Setup | Lightning App Builder and “Create a new Lightning Page”
Select “App Page” and give the label in the next screen as “My_Spring_Boot_Canvas_App”
Select “One region” in the next step and click “Finish”
This will take us to the screen where we can select out previously created Lightning Component “SpringBootCanvasApp”. Drop this app on the page and “Activate” the page after “Save”

Now add this Lighning page on the Navigation on one of your Apps in Salesforce. When we go to this page “My_Spring_Boot_Canvas_App” , we should be able to see the Spring boot application load within the Lightning Component in that page. (Make sure that your Spring Boot server is still running and not shutdown). On the Spring boot server, you should be able to see the signed_request logged on the console.

  1. Canvas SDK and Refreshing a signed request.
    Canvas provides a framework (Canvas SDK) that can be used to perform various operations on Salesforce. In our next step we will see how we can access the sdk and then use it to get the signed request within the UI (javascript) part of the application. We will, for now, provide inline javascript within our Thymeleaf teamplate. These could be replaced with your choice of frameworks/libraries/templates. The basics remain the same.

Canvas provides a framework (Canvas SDK) that can be used to perform various operations on Salesforce. In our next step we will see how we can access the sdk and then use it to get the signed request within the UI (javascript) part of the application. We will, for now, provide inline javascript within our Thymeleaf teamplate. These could be replaced with your choice of frameworks/libraries/templates. The basics remain the same.

Here is the final version of what our index.html template looks like now. We have done the following .. on the load of the page in the browser, we will add an onclick event on the button. The click of the button, calls the refreshSignedRequest on the Canvas SDK (this is the current way to get the signed request within the UI). On getting a successful response, it logs the signedRequest on the console else it logs the error status. Note: Replace the “your-salesforce-instance-name-here” with the instance name of your Salesforce and correct the version number as necessary.

<html>
<head>
	<script type="text/javascript" src="<your-salesforce-instance-name-here>/canvas/sdk/js/48.0/canvas-all.js"></script>
	<script type="text/javascript">
	window.onload = (event) => {
		var ctxlink = Sfdc.canvas.byId("ctxlink");
		ctxlink.onclick = function(){
			Sfdc.canvas.client.refreshSignedRequest(function(data) {
				if(data.status==200){
					var signedRequest = data.payload.response;
					console.log(signedRequest);
				}
				else{
					console.log('Status received != 200. Status is ' + data.status);
				}
			});
		}
	};
	
	</script>
	
</head>
<body>
	<h1>Main Index Page</h1>
	<p> Canvas Page Content</p>
	
	<input type="text"/>
	<button id="ctxlink" >Some button</button>
</body>
</html>
  1. Decoding and verifying a signed request.
    So, what does the signed request of Canvas consist of ? The details of what the signed request is can be found at the end of this documentation here (https://developer.salesforce.com/docs/atlas.en-us.platform_connect.meta/platform_connect/canvas_app_signed_req_authentication.htm). Verifying and decoding of the signed request can be done in this way : https://developer.salesforce.com/docs/atlas.en-us.platform_connect.meta/platform_connect/canvas_app_unsigning_code_example.htm

We will create a decoder class Decoder.class with the following code ..

package com.wordpress.codesilo.controller;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Component;

@Component
public class Decoder {

	public void decodeSignedRequest(String signedRequest) {
		try {
			String signedRequestVal = signedRequest.split("signed_request=")[1];
			
			String[] signedRequestArray = signedRequestVal.split("[.]");
			String clientSecret = "your-client-secret-from-canvas-connected-app-here";
			String encodedSignature = signedRequestArray[0];
			String endcodedEnvelope = signedRequestArray[1];

			Mac sha256HMAC = Mac.getInstance("HMACSHA256");
			SecretKey hmacKey = new SecretKeySpec(clientSecret.getBytes(), "HMACSHA256");
			sha256HMAC.init(hmacKey);

			byte[] digest = sha256HMAC.doFinal(endcodedEnvelope.getBytes("UTF-8"));
			byte[] decode_sig = new Base64(true).decode(encodedSignature);

			if (Arrays.equals(digest, decode_sig)) {
				System.out.println("Valid signed request found");
			} else {
				System.out.println("Invalid signed request found");
			}

			// The following is the CanvasRequest object generated from Salesforce
			String jsonEnvelope = new String(new Base64(true).decode(endcodedEnvelope));
			System.out.println(jsonEnvelope);

		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalStateException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

Replace the client secret from the secret from your instance of Salesforce above.

We will then call this decoder.decodeSignedRequest method from our main controller class. We will change the controller class a bit to make sure the request body is URL decoded. This is what the class looks like..

@Controller
public class HomeController {

	@Autowired
	private Decoder decoder;
	
	@RequestMapping(value = "/", method = RequestMethod.POST)
	public String index(@RequestBody String signedRequest) {
		try {
			String signedRequestDecoded = URLDecoder.decode(signedRequest, StandardCharsets.UTF_8.name());
			System.out.println(signedRequestDecoded);
			decoder.decodeSignedRequest(signedRequestDecoded);
		}
		catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return "index";
	}
	
}

Note: The code shown does not cover areas like error handling or other best practices. That is left for the reader to add.

  1. Looking at the Canvas Request object
    In the Decoder class above, we log a jsonEnvelope string. This is the String version of the CanvasRequest object. (https://developer.salesforce.com/docs/atlas.en-us.platform_connect.meta/platform_connect/canvas_request_object.htm). Something like Jackson can be used to parse this into an Object.
  1. Creating an event from the Application
    Now we will take a look at a simple case of rasing an event from the Spring boot application and consuming that on the Lightning component allowing a client – client communication between the Canvas App and the parent (the Lightning component).
    We will change the index.html file to parse the signedRequest to an Object, get a “client” from it and then use the “publish” method on the sdk to publish a message. Here is what the final “onclick” method will look like.
ctxlink.onclick = function(){
			Sfdc.canvas.client.refreshSignedRequest(function(data) {
				if(data.status==200){
					var signedRequestResponse = data.payload.response;
					console.log(signedRequestResponse);
					
					var signedReqObj = JSON.parse(Sfdc.canvas.decode(signedRequestResponse.split('.')[1]));
		    	    var client = signedReqObj.client;

					Sfdc.canvas.client.publish(
		    	            client,
		    	            {name : "mynamespace.statusChanged", payload : {status : 'Message Sent'}});
					
				}
				else{
					console.log('Status received != 200. Status is ' + data.status);
				}
			});
		};
  1. Consuming the event on the Lightning Component
    Now on the Lightning Component on Salesforce we will make a couple of changes to listen to the Message. We will first add onCanvasAppLoad attribute to the component canvasApp. The component will now look as follows:
<aura:component implements="flexipage:availableForAllPageTypes,flexipage:availableForRecordHome">
	<lightning:card>
    <force:canvasApp developerName="My_Spring_Boot_Canvas_App" maxHeight="200" maxWidth="200" onCanvasAppLoad="{!c.onCanvasLoad}"/>
        </lightning:card>
</aura:component>

Now, we will add a controller and add the following code to the controller.

({
	onCanvasLoad : function(component, event, helper) {
		console.log('Canvas Loaded');
        window.addEventListener('message', function (event) {
            var data = JSON.parse(event.data);
            if (data.targetModule === 'Canvas' && 
                data.body && 
                data.body.event && 
                data.body.event.name === 'mynamespace.statusChanged')
            console.log('payload from canvas app', data.body.event.payload);
        }, false);
	}
})

Now, when we go to the Canvas App and click on the button, the console should log the messages from the Lightning App.

Update: Posting messages using iFrame instead of Canvas can be found in this blog here: (https://codesilo.wordpress.com/2020/07/24/salesforce-consuming-messages-on-iframe-from-spring-boot-app/)

References:

https://salesforce.stackexchange.com/questions/131245/how-to-fire-an-event-from-a-canvas-app-to-a-lightning-component

https://salesforce.stackexchange.com/questions/106538/canvas-app-signedrequest-is-null-on-postback

https://salesforce.stackexchange.com/questions/76869/canvas-signed-request-sr-in-visualforce

Wiki Revisions History

I recently wanted to get some sample data for some of my nosql trials and decided to search for some wiki metadata. More specifically, the history of the revisions. Very soon I realized that many folks have already built applications on it and that there is extensive API available to get the data.

I could make the data set by running throught the API and getting revisions on all pages. However, scraping isn’t a good idea and media wiki limits the results for the same reason.

For the info : API calls can be made by referring to the documentation here http://www.mediawiki.org/wiki/API

As an example for the API call, if we need to find the revisions for a page named “Geography_of_Afghanistan”, we could use the following call…

http://en.wikipedia.org/w/api.php?action=query&prop=revisions&titles=Geography_of_Afghanistan&rvprop=ids|timestamp|user&rvlimit=5000

And the following call would also give us the comments

http://en.wikipedia.org/w/api.php?action=query&prop=revisions&titles=Geography_of_Afghanistan&rvprop=ids|timestamp|user|comment&rvlimit=5000

Notice that although we use the rvlimit as 5000 , the results are limited to 500 and we also get the message with the response ..

“rvlimit may not be over 500 (set to 5000) for users” 

To get the complete data set , media wiki provided data dumps that can be downloaded. Refer to this link for the dumps .http://dumps.wikimedia.org/enwiki/20110317/

What we need is the meta history. Once I downloaded the dump I realised that the latest xsd was not available for the data set. The latest xsd doc supplied by media wiki is at http://www.mediawiki.org/xml/export-0.4.xsd ,but, we need the export-0.5.xsd to work with the downloaded dumps.

So, to solve the problem above, I downloaded trang. trang can be used to generate xsd from xml. Here is a good write-up to get an idea.

I will add the export-0.5.xsd that got generated to this blog. Hope it helps other till the xsd is published by media wiki.

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.mediawiki.org/xml/export-0.5/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:export-0.5="http://www.mediawiki.org/xml/export-0.5/">
  <xs:element name="mediawiki">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="export-0.5:siteinfo"/>
        <xs:element maxOccurs="unbounded" ref="export-0.5:page"/>
      </xs:sequence>
      <xs:attribute name="version" use="required" type="xs:decimal"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="siteinfo">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="export-0.5:sitename"/>
        <xs:element ref="export-0.5:base"/>
        <xs:element ref="export-0.5:generator"/>
        <xs:element ref="export-0.5:case"/>
        <xs:element ref="export-0.5:namespaces"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="sitename" type="xs:NCName"/>
  <xs:element name="base" type="xs:anyURI"/>
  <xs:element name="generator" type="xs:string"/>
  <xs:element name="case" type="xs:NCName"/>
  <xs:element name="namespaces">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" ref="export-0.5:namespace"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="namespace">
    <xs:complexType mixed="true">
      <xs:attribute name="case" use="required" type="xs:NCName"/>
      <xs:attribute name="key" use="required" type="xs:integer"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="page">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="export-0.5:title"/>
        <xs:element ref="export-0.5:id"/>
        <xs:element minOccurs="0" ref="export-0.5:redirect"/>
        <xs:element minOccurs="0" ref="export-0.5:restrictions"/>
        <xs:element maxOccurs="unbounded" ref="export-0.5:revision"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="title" type="xs:string"/>
  <xs:element name="redirect">
    <xs:complexType/>
  </xs:element>
  <xs:element name="restrictions" type="xs:string"/>
  <xs:element name="revision">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="export-0.5:id"/>
        <xs:element ref="export-0.5:timestamp"/>
        <xs:element ref="export-0.5:contributor"/>
        <xs:element minOccurs="0" ref="export-0.5:minor"/>
        <xs:element minOccurs="0" ref="export-0.5:comment"/>
        <xs:element ref="export-0.5:text"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="timestamp" type="xs:NMTOKEN"/>
  <xs:element name="contributor">
    <xs:complexType>
      <xs:choice minOccurs="0">
        <xs:element ref="export-0.5:ip"/>
        <xs:sequence>
          <xs:element ref="export-0.5:username"/>
          <xs:element ref="export-0.5:id"/>
        </xs:sequence>
      </xs:choice>
      <xs:attribute name="deleted" type="xs:NCName"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="ip" type="xs:string"/>
  <xs:element name="username" type="xs:string"/>
  <xs:element name="minor">
    <xs:complexType/>
  </xs:element>
  <xs:element name="comment">
    <xs:complexType mixed="true">
      <xs:attribute name="deleted" type="xs:NCName"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="text">
    <xs:complexType>
      <xs:attribute name="bytes"/>
      <xs:attribute name="deleted" type="xs:NCName"/>
      <xs:attribute name="id" type="xs:integer"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="id" type="xs:integer"/>
</xs:schema>

AJAX using JQuery

July 12, 2010 Leave a comment

We follow the same steps as in the previous blog entry except that we use a different javascript framework this time. The only thing that changes is the javascript in the code.

The javascript function now changes to…


$.ajax({
url: "../upperCaseSubmit.do",
type: "GET",
data: $('#textForm').serialize(),
cache: false,
success: function (response) {
$('#result').html(response).fadeIn('fast');
$('#result').html(response).fadeOut(2000);
},
error: function(){
alert("Error in processing the request");
}
});

Note: I have included some animation (fadeIn and fadeOut) which is not there in the code I created with Prototype.
Ajax.Responders in Prototype can be replaced by Jquery’s Ajax Events.