String.fromCharArray is going to create a UTF-8 encoded string, which will alter the bytes in the stream. For example, 242 will be encoded as two bytes instead of one. Any value outside 0-127 will be encoded as multiple bytes when using this function, including all negative integers. Since we can't really work with binary data directly in Apex Code, just store the base-64 encoded value directly in a string or some persistent storage (e.g. a custom setting).
public class Base64 {
static String[] codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('');
// Encodes integers 0-255 into base 64 string
public static String encode(Integer[] source) {
// Preallocate memory for speed
String[] result = new String[(source.size()+2)/3];
// Every three bytes input becomes four bytes output
for(Integer index = 0, size = source.size()/3; index < size; index++) {
// Combine three bytes in to one single integer
Integer temp = (source[index]<<16|source[index+1]<<8)|source[index+2];
// Extract four values from 0-63, and use the code from the base 64 index
result[index]=codes[temp>>18]+(codes[(temp>>12)&63])+(codes[(temp>>6)&63])+codes[temp&63];
}
if(Math.mod(source.size(),3)==1) {
// One byte left over, need two bytes padding
Integer temp = (source[source.size()-1]<<16);
result[result.size()-1] = codes[temp>>18]+(codes[(temp>>12)&63])+codes[64]+codes[64];
} else if(Math.mod(source.size(),3)==2) {
// Two bytes left over, need one byte padding
Integer temp = (source[source.size()-2]<<16)|(source[source.size()-1]<<8);
result[result.size()-1] = codes[temp>>18]+(codes[(temp>>12)&63])+(codes[(temp>>6)&63])+codes[64];
}
// Join into a single string
return String.join(result, '');
}
}
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
Best Answer
Convert the string in URL encode
Which will replace it to
Now in class simple get it from apex using
apexpages.currentpage().getparameters().get('parameter Name');
and use it you will get expected output.