Friday, January 25, 2013

[Salesforce / Canvas] Set up a minimum Canvas App (Java style)

This new post is about setting up a Force.com Canvas App and doing beautiful things with it. It comes from an old challenge I won (see Cloudspokes.com blog post), evenif in that case I used Javascript to get the session infos.

What is a Canvas App? In simple words it is a way to integrate external custom web application into Force.com platform, with simplified authorization process.
There are two ways of authentication:
  • OAuth 2.0 (GET)
  • Signed Request (POST)
I'm going to use the last method.

The first thing you have to do is creating a new "Connected App" in Setup->App Setup->Create->Apps->Connected Apps and press the "New" button.

Now set the mandatory fields:
  • Name: yuor application name
  • Contact email: your email
  • Callback URL: this is the callback URL used in the OAuth process. Simply set with the app URL
  • Selected OAuth Scopes: permissions associated to the injected session informations
  • Force.com Canvas: yes it is
  • Canvas App URL: endpoint of your app
  • Access method: Signed Request (POST)

For the sake of testing, you can set "http://localhost[:port]" as the endpoint.

Here is my configuration:



If you click in the "Chatter" tab you now see the new app:


The second step is to host a local web app.
To simplify your play here is a simple Java Play! Application that integrates with Force.com Canvas:

https://github.com/enreeco/CanvasApp-Base

To run the app simply follow my previous post [Java Play! / Heroku] Setup Java Play! + Heroku + Eclipse, for the Play part (if you want you can also use Heroku and configure the whole thing to use Heroku, but it can be a bit frustrating when developing, due to the loss of time in deploying).

How it works?

Salesforce call your app making a POST request, sending a "signed_request" parameter that is a big base64 encoded String in which all session infos are stored. This is an example:

jCblE7oIsZanRIeshRkJxtx2Dk1tS2tP3GWL0Yf1o+s=.eyJjb250ZXh0Ijp7InVzZXIiOn QiOmZhbHNlLCJjdXJyZW5jeUlzb0NvZGUiOiJFVVIifSwibGlua3MiOnsiZW50ZXJwcmlzZVVybCI6Ii 9zZXJ2aWNlcy9Tb2FwL2MvMjYuMC8wMERpMDAwMDAwMEg0bXUiLCJtZXRhZGF0YVVybCI6Ii9zZXJ2aW Nlcy9Tb2FwL20vMjYuMC8wMERpMDAwMDAwMEg0bXUiLCJwYXJ0bmVyVXJsIjoiL3NlcnZpY2VzL1NvYX IsImZ1bGxOYW1lIjoiQWRt ..... wiY2xpZW50SWQiOiIzTVZHOUEya04zQm4xN2h2Vn Uuc2FsZXNmb3JjZS5jb20ifQ==

Using the App Consumer Secret (red circle in the first pic) the Java Salesforce Framework SDK decodes this response and verify that it isn't tamped with.
The SDK can be found here, but it has been included in the CanvasApp-Base github under the "com.salesforce.canvas" package.

N.B. I've made some modification to the SignedRequest class (it appears that my own libraries have a new version and the org.apache.commons.codec.binary.Base64 class has different constructors than the one used in the SDK...so if you have problems just replace this class with the one provided by Salesforce).
The Consumer Secret is stored in the "application.conf" file, in the first lines:
     # Force.com User Secret
     canvas.consumer.secret = XXXXXXXX
This value is accessed through the AppProperties class.

This is the result:

The app uses sessions to store Canvas session info, so if you reload the page:

The application now has a valid Session Id that can be used, accordingly to the App Access Level, to work with Salesforce (from queries to metadata describes).

The core of this web app is the "Application.index()" method that stores the logic of the Signed Request handling:
package controllers;

import play.mvc.Controller;
import play.mvc.Result;
import views.html.index;

import com.salesforce.canvas.CanvasRequest;
import com.salesforce.canvas.SignedRequest;

public class Application extends Controller {

 public static Result index() {

  String message = "";
  CanvasRequest canvasInfo = AppProperties.getSessionCanvasRequest(Controller.session());
  if(canvasInfo != null)
  {
   message += "Hi <b>"+canvasInfo.getContext().getUserContext().getUserName()+"</b>! Canvas Session Info has already been given. Enjoy Canvas App!";
  }
  else
  {
   message += "Canvas App Session info not already set.";
   if(request().method()== "POST")
   {
    message += "Incoming POST request...";
    
    //this is the post info got from Force.com POST request
    String[] signedRequest = request().body().asFormUrlEncoded().get("signed_request");
    
    if(signedRequest!=null)
    {
     System.out.println("Request: "+signedRequest[0]);
     try
     {
      canvasInfo = SignedRequest.verifyAndDecode(signedRequest[0], AppProperties.CANVAS_CONSUMER_SECRET);
      AppProperties.setSessionCanvasRequest(canvasInfo,Controller.session());
      message += "Hi <b>"+canvasInfo.getContext().getUserContext().getUserName()+"</b>! Canvas Session Info has just been correctly given. Enjoy Canvas App!";
     }
     catch(Exception e)
     {
      e.printStackTrace();
      message += "Error occurred (see log): "+e.getLocalizedMessage();
     }
    }
    else
    {
     message += "No session info provided.";
    }
   }
  }
  return ok(index.render(message));
 }

}
And this is the "routes" file:
  GET      /                   controllers.Application.index()
  POST     /                   controllers.Application.index()
The "Application.index()" method serves both GET/POST requests.
Finally this is what happens if you access the page using a simple GET request outside Canvas:

That's all.