Pages

Saturday, January 29, 2011

How to use FB.Canvas.setAutoResize

FB.Canvas.setAutoResize() function from Facebook Javascript SDK can be used to start or stops a timer which resizes your iframe every few milliseconds.

This function is useful when we know our page will resize but don't know when it will happen. A possible scenario is a Canvas application where every page has different height due to variable size of content.

It is very easy to use this function as per facebook documentation. But I faced some issues when I used it in Visual Force pages.

Here is Visual Force Page code to use this feature:


<apex:page>
<body style="overflow: hidden">
<div id="fb-root"></div>
<script>
window.fbAsyncInit = function() {
FB.Canvas.setAutoResize();
};
(function() {
var e = document.createElement('script'); e.async = true;
e.src = document.location.protocol + '//connect.facebook.net/en_US/all.js';
document.getElementById('fb-root').appendChild(e);
}());
</script>

<div style="height:2000px; width:500px; background:blue;">
test page
</div>
</body>
</apex:page>


The main hack is body style="overflow: hidden" which is not documented anywhere but it is required to get a consistent working page in all browser.

Thursday, January 27, 2011

Facebook Application Deauthorization

When a user of your Facebook app removes it, your app can be notified by specifying a Deauthorize Callback URL in the Developer App. During app removal facebook will send an HTTP POST request containing a single parameter, signed_request, which contains the user id (UID) of the user that just removed your app.

Using this user id we can take necessary action to clean up any user data, free SalesForce license and any other other task as per business requirement.

Facebook doesn't send any user access token in this request and all existing user access tokens will be automatically expired.

While integrating facebook with Salesforce we create a portal user for every authenticated facebook user which consume a "Customer Portal license", so when a user remove your app from facebook it will be good to free that license. We can free Salesforce license by deactivating the user.

So let's see how this works....

Step 1: Create a VisualForce Page named "PostRemoveCallback" and place the code below.


<apex:page controller="PostRemoveCallbackController" action="{!deactivateUser}">
<apex:messages style="color:red;"/>
</apex:page>


Step 2: Create Controller Class for page named "PostRemoveCallbackController" and place the code below.


public with sharing class PostRemoveCallbackController
{
private string signedRequest;
private string userId;
private string algorithm;
private string encodedSignature;
private string encodedExpectedSignature;

public PostRemoveCallbackController()
{
try
{
if(ApexPages.currentPage().getParameters().get('signed_request')!= NULL
&& ApexPages.currentPage().getParameters().get('signed_request')!='')
{
signedRequest = ApexPages.currentPage().getParameters().get('signed_request');

System.Debug(System.LoggingLevel.Info, 'signedRequest :::' + signedRequest);

// Split the parameter based on period(.), as signed_request has two parts:
//1. HMAC SHA-256 Signature String
//2. base64url encoded JSON object
//More Info at : http://developers.facebook.com/docs/authentication/canvas
String[] itms = signedRequest.split('[.]',0);

//Decoded Signature, returning Blob
Blob decodedSignature = EncodingUtil.base64Decode(itms[0].replace( '-', '+').replace( '_', '/'));

//Decoded Payload, returning Blob
Blob decodedPayload = EncodingUtil.base64Decode(itms[1].replace( '-', '+').replace( '_', '/'));

//Encoded Signature, as this will be matched with Expected Signature.
encodedSignature = EncodingUtil.base64Encode(decodedSignature);

// Payload Blob converted to String as it contains FBUserId which is to be extracted for further operations.
//Sample format:::
//{"algorithm":"HMAC-SHA256","issued_at":1285334964,"user_id":"12345"}
String strPayload = decodedPayload.toString();
System.Debug(System.LoggingLevel.Info, 'JSON object :::' + strPayload);

//Instance of JSONObject created, to extract FBUserId and Algorithm.
JSONObject jsonObj = new JSONObject(strPayload);

//Get FBUserId string
userId = jsonObj.getString('user_id');

//Get Algorithm string
algorithm = jsonObj.getString('algorithm');

if (algorithm.toUpperCase() != 'HMAC-SHA256')
{
System.debug(Logginglevel.INFO, 'Unknown algorithm. Expected HMAC-SHA256');
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'Unknown algorithm. Expected HMAC-SHA256'));
}

encodedExpectedSignature= EncodingUtil.base64Encode(Crypto.generateMac('HmacSHA256', Blob.valueOf(itms[1].replace( '-', '+').replace( '_', '/')),
Blob.valueOf(BiaSettings__c.getInstance().FacebookSecretKey__c)));

if (encodedSignature != encodedExpectedSignature)
{
System.debug(Logginglevel.INFO,'Bad Signed JSON signature!');
ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'Bad Signed JSON signature!'));
}
}
}
catch(Exception ex)
{
ApexPages.addMessages(ex);
}
}

public void deactivateUser()
{
try
{
//Check the conditions and Deactivate User if conditions are fullfilled.
if(userId !='' && algorithm.toUpperCase() == 'HMAC-SHA256' && encodedSignature == encodedExpectedSignature)
{
//this business logic is for requirement to deactivate user.
User user = [Select Id, IsActive from User where username =: 'user_' + userId + '@abc.com'];

user.IsActive = false;
update user;
}
}
catch(Exception ex)
{
ApexPages.addMessages(ex);
}
}
}


Step 3:Here is testclass for controller class to fulfill Force.com unit test coverage.

@isTest
private class PostRemoveCallbackControllerTest {

static testMethod void deactivateUser_test1()
{
//To Do : Write code here to create a portal user and assign it's facebook user Id to variable facebookUserID, I have used strings padded with zeros in left as facebook will never generate those ids
User portalUser = null;
String facebookUserID = '0123';

//JSON String that is generated by Facebook when a user Remove application from his profile
//Facebook encode this JSON String using the mechanism mentioned in algorithm property.
String strPayload = '{"algorithm":"HMAC-SHA256","issued_at":1295697872,"user":{"locale":"en_US","country":"in"},"user_id":"' + facebookUserID + '"}';

//Decoded Payload, returning Blob
Blob decodedPayload = Blob.valueOf(strPayload);

//Encode payload to get an unencoded String representing its normal form. After encoding a equal(=) is added for padding, remove this it will
//Create problem in our test method because fecebook also remove this when signing the request
String encodedPayload = EncodingUtil.base64Encode(decodedPayload).replace( '+', '-').replace( '/', '_').replace('=','');

//First encrypt encodedPayload using HmacSHA256 algorith and then encode this.
//After this remove unnecessary equal(=) character
string encodedSignature = EncodingUtil.base64Encode(Crypto.generateMac('HmacSHA256', Blob.valueOf(encodedPayload),
Blob.valueOf(BiaSettings__c.getInstance().FacebookSecretKey__c))).replace( '+', '-').replace( '/', '_').replace('=','');

//Build a Signed request that will be send to page
String signed_request = encodedSignature + '.' + encodedPayload;

ApexPages.currentPage().getParameters().put('signed_request', signed_request);

PostRemoveCallbackController controller = new PostRemoveCallbackController();
controller.deactivateUser();

portalUser = [Select ID, Username, isActive from User where id=: portalUser.id limit 1];
System.assertEquals(portalUser.isActive, false);
}

static testMethod void deactivateUser_test2()
{
//To Do : Write code here to create a portal user and assign it's facebook user Id to variable facebookUserID, I have used strings padded with zeros in left as facebook will never generate those ids
User portalUser = null;
String facebookUserID = '0123';

//JSON String that is generated by Facebook when a user Remove application from his profile
//Facebook encode this JSON String using the mechanism mentioned in algorithm property.
String strPayload = '{"algorithm":"MD5","issued_at":1295697872,"user":{"locale":"en_US","country":"in"},"user_id":"' + facebookUserID + '"}';

//Decoded Payload, returning Blob
Blob decodedPayload = Blob.valueOf(strPayload);

//Encode payload to get an unencoded String representing its normal form. After encoding a equal(=) is added for padding, remove this it will
//Create problem in our test method because fecebook also remove this when signing the request
String encodedPayload = EncodingUtil.base64Encode(decodedPayload).replace( '+', '-').replace( '/', '_').replace('=','');

//First encrypt encodedPayload using HmacSHA256 algorith and then encode this.
//After this remove unnecessary equal(=) character
string encodedSignature = EncodingUtil.base64Encode(Crypto.generateMac('HmacSHA256', Blob.valueOf(encodedPayload),
Blob.valueOf(BiaSettings__c.getInstance().FacebookSecretKey__c))).replace( '+', '-').replace( '/', '_').replace('=','');

//Build a Signed request that will be send to page
String signed_request = encodedSignature + '.' + encodedPayload;

ApexPages.currentPage().getParameters().put('signed_request', signed_request);

PostRemoveCallbackController controller = new PostRemoveCallbackController();
}
}


And you are done with coding, now its time for some mouse clicks..

Ensure that you have added newly create Visual Force Page in list of "Site VisualForce Pages" for Force.com site of your Facebook application.

Add page url to "Deauthorize Callback" field on "Advanced" tab of your Facebook App Settings. Facebook changes it's configuration UI quite frequently so if you are not able to locate this please check latest documentation.

e.g http://ForceSiteDomain.com/PostRemoveCallback

And you are done. Try removing your application from facebook and if you have a corresponding user in Salesforce that will be deactivated.

I have used JSONObject class to read JSON string contained in signed_request. You can download JSON Object class from here.

Facebook Reference(s):
http://developers.facebook.com/docs/authentication/