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