[SalesForce] Canvas app – verify signed request

i am following the canvas app documentation to verify a signed request before using the parameters i am passing in the canvas app request. The canvas app is embedding a third party app in salesforce.

From documentation:

  1. Receive the POST message that contains the initial signed request from
    Salesforce.
  2. Split the signed request on the first period. The result is two strings: the hashed Based64 context signed with the consumer secret
    and the Base64 encoded context itself.
  3. Use the HMAC SHA-256 algorithm to hash the Base64 encoded context and sign it using your consumer secret.
  4. Base64 encode the string created in the previous step.

5.Compare the Base64 encoded string with the hashed Base64 context signed with the consumer secret you received in step 2.

I followed the below instructions

For step 1

Map<String, String> pageParams = ApexPages.currentPage().getParameters();
     String sSignedReq = pageParams.get('signed_request');

For step 2:

 List<String> outputval = sSignedReq.split('\\.');

For step 3:

 Blob encodedEnvelopeHash = Crypto.generateDigest('SHA-256', Blob.valueOf(outputval[1])); 
String Privatekey = 'XXX';
blob signedvalue = Crypto.sign('RSA-SHA256', encodedEnvelopeHash, encodingutil.base64Decode(Privatekey));

I used open ssl to generate private key(PKCS8 format) for using it in crypto sign function

Link here:

Relevant code:

// Generate your own certificate - subject information is redacted
openssl req -x509 -nodes -subj "/C=XX/ST=XXX/L=XXX/CN=www.xxx.com" -days 365 -newkey rsa:1024 -keyout key.pem -out cert.pem
// Output the private key onto the terminal in PKCS#8 format
openssl pkcs8 -topk8 -nocrypt -in key.pem -outform PEM

For step 4:

String finalouput = EncodingUtil.base64Encode(signedvalue);

In step 5 i am doing a comparison and the 2 strings are never the same.

   if(outputval[0] == finalouput)
     system.debug('datamatchingok');
     else
      system.debug('datanotmatchingok');  

What am i doing wrong? and how do i use consumer secret for verifying the signed request. Step 3 of documentation says we need to use that but i wasn't able to wrap my head around the documentation to understand where to use it?

Best Answer

Here is a step-by-step click path to create a Salesforce Canvas App that uses a POSTed Signed Request to a Visualforce Page with an Apex Controller (and the Apex Controller successfully validates the Signed Request).

Note that in practice, I have always had Canvas Apps pointing to external platforms such as Heroku Express/Node, Java or Ruby. So I typically do the signed request validation in another language (e.g. javascript in Node.js). But to answer your specific question, I have included sample code for Visualforce and Apex and we will be validating the signed request using pure Apex. Also note that you do NOT need your own self-signed certificate or any use of openssl.

Here you go... this exercise will take about 15-30 minutes to complete. Let me know if you have any issue or questions and I'll see if I can assist further.

Step 1 (use Classic)

  • Switch to Salesforce Classic -> Setup (to best align to this click path)

Step 2 (create and configure a new Canvas Connected App) -

  • Create -> Apps -> New Connected App

    • Basic Information
      • Connected App Name: Canvas Test
      • API Name: Canvas_Test
      • Contact Email: (your email)
    • API (Enable OAuth Settings) (This is needed to get the Secret Key)
      • Enable OAuth Settings: checked
      • Callback URL: https://localhost/callback-not-used
      • Selected OAuth Scopes: Full access (full)
      • Require Secret for Web Server Flow: checked (don't think this matters to example, but since it's checked by default, leave it)
    • Canvas App Settings
      • Canvas Checkbox: checked
      • Canvas App URL: normally external like heroku/node app, but for our demo purposes... https://mydomain--sandbox--c.cs16.visual.force.com/apex/canvas_page ( --- NOTICE THE "c.", I got this URL by navigating to /apex/canvas_page from my internal salesforce instance and then copy/pasted what it automatically changed to. We will create a Visualforce page soon, so you may have to come back to edit this if you can't guess it based on your environment)
      • Access Method: Signed Request
      • Locations: (wherever you will embed the canvas app)
    • Save, Continue
    • Remember, any changes to Connected App settings can take 10 full minutes to propagate
  • Connected App Details for "Canvas Test"

    • You should now have a Consumer Key and Consumer Secret (click to reveal)
      • Consumer Key: (CONSUMER KEY REDACTED)
      • Consumer Secret: (CONSUMER SECRET REDACTED)
  • Create -> Apps -> Connected Apps

    • Manage -> Edit Policies Button
      • OAuth Policies
        • Permitted Users: Admin approved users are pre-authorized
        • Save
    • Manage -> (scroll down to relate lists) Manage Profiles Button (or Permission Sets)
      • Add System Administrator (or whatever is appropriate)

Step 3 (create an Apex controller)

  • Developer Console
    • File -> New -> Apex Class
      • Name: canvas_page_ctlr
      • Code:

Apex

public class canvas_page_ctlr {

    public String rawOutputToScreen {get; set;}

    public canvas_page_ctlr(){
        //seriously, store this somewhere other than code (e.g. Private Custom Setting)
        String CLIENT_SECRET = 'INSERT_YOUR_CONSUMER_SECRET_HERE'; 

        String signedRequest = ApexPages.currentPage().getParameters().get('signed_request');
        if(String.isNotBlank(signedRequest)){
            system.debug('signed_request: ' + signedRequest);
            List<String> signedRequestPieces = signedRequest.split('\\.'); //signed request pieces separated by a period (must escape the period in split regex)
            String contextSignedWithClientSecretByCanvasApp = signedRequestPieces[0]; //signed
            String contextEncoded  = signedRequestPieces[1]; //just encoded

            //Re-sign the encoded context with our pre-shared Canvas Connected App client secret...
            String contextReSignedWithClientSecretByThisController = 
                EncodingUtil.base64Encode(Crypto.generateMac('hmacSHA256', Blob.valueOf(contextEncoded), Blob.valueOf(CLIENT_SECRET)));

            //The signatures should match...
            if(contextSignedWithClientSecretByCanvasApp==contextReSignedWithClientSecretByThisController){
                //context valid and untampered...
                rawOutputToScreen = EncodingUtil.base64Decode(contextEncoded).toString();
            } else {
                //signature mismatch. don't trust context...
                rawOutputToScreen = 'Sorry Charlie! Not today.';
            }
        } else {
            //we didn't get a signed_request parameter in the POST body... show us what we got for demo purposes only...
            rawOutputToScreen = 'Error: ' + ApexPages.currentPage().getParameters();
        }
    }
}

Step 4 (create a Visualforce page) - File -> New -> Visualforce Page - Name: canvas_page - Code:

Visualforce

<apex:page controller="canvas_page_ctlr" sidebar="false" showHeader="false" >
<div style="border: solid 2px red; width: 400px; height: 400px; padding: 10px;">
    <p><u>Canvas via Signed Request Demo</u></p>
    <p>{!rawOutputToScreen}</p>
</div>

Step 5 (preview the Canvas App and the parameters) - Setup -> App Setup -> Canvas App Previewer - Click on "Canvas Test" - You should see the Canvas App delivering a successfully verfied signed request in this format:

JSON context data structure sent with Canvas Signed Request

{
  "algorithm": "HMACSHA256",
  "issuedAt": -2038422695,
  "userId": "005-redacted",
  "client": {
    "refreshToken": null,
    "instanceId": "_:Canvas_Test:",
    "targetOrigin": "-redacted",
    "instanceUrl": "-redacted",
    "oauthToken": "00D-redacted"
  },
  "context": {
    "user": {
      "userId": "005-redacted",
      "userName": "b-redacted",
      "firstName": "First",
      "lastName": "Last",
      "email": "b-redacted",
      "fullName": "First Last",
      "locale": "en_US",
      "language": "en_US",
      "timeZone": "America\/Chicago",
      "profileId": "00e-redacted",
      "roleId": "00E-redacted",
      "userType": "STANDARD",
      "currencyISOCode": "USD",
      "profilePhotoUrl": "-redacted",
      "profileThumbnailUrl": "-redacted",
      "siteUrl": null,
      "siteUrlPrefix": null,
      "networkId": null,
      "accessibilityModeEnabled": false,
      "isDefaultNetwork": true
    },
    "links": {
      "loginUrl": "-redacted",
      "enterpriseUrl": "-redacted",
      "metadataUrl": "-redacted",
      "partnerUrl": "-redacted",
      "restUrl": "-redacted",
      "sobjectUrl": "-redacted",
      "searchUrl": "-redacted",
      "queryUrl": "-redacted",
      "recentItemsUrl": "-redacted",
      "chatterFeedsUrl": "-redacted",
      "chatterGroupsUrl": "-redacted",
      "chatterUsersUrl": "-redacted",
      "chatterFeedItemsUrl": "-redacted",
      "userUrl": "-redacted"
    },
    "application": {
      "name": "Canvas Test",
      "canvasUrl": "https:\/\/redacted--c.cs16.visual.force.com\/apex\/canvas_page",
      "applicationId": "06Pf-redacted",
      "version": "1.0",
      "authType": "SIGNED_REQUEST",
      "referenceId": "09H-redacted",
      "options": [

      ],
      "samlInitiationMethod": "None",
      "isInstalledPersonalApp": false,
      "namespace": "",
      "developerName": "Canvas_Test"
    },
    "organization": {
      "organizationId": "00D-redacted",
      "name": "B-redacted",
      "multicurrencyEnabled": false,
      "namespacePrefix": null,
      "currencyIsoCode": "USD"
    },
    "environment": {
      "referer": null,
      "locationUrl": "-redacted",
      "displayLocation": null,
      "sublocation": null,
      "uiTheme": "Theme3",
      "dimensions": {
        "width": "800px",
        "height": "900px",
        "maxWidth": "1000px",
        "maxHeight": "2000px",
        "clientWidth": "1123px",
        "clientHeight": "80px"
      },
      "parameters": {
        "my_custom_param_1": "if_provided_from_canvas_location",
        "my_custom_param_2": "if_provided_from_canvas_location",
      },
      "record": {

      },
      "version": {
        "season": "SUMMER",
        "api": "43.0"
      }
    }
  }
}

Cheers,

Bryan Isbell

Salesforce Certified Technical Architect, CISSP