Permalink
Browse files

Merge pull request #6 from metadaddy-sfdc/master

Encrypt secrets
  • Loading branch information...
2 parents 17d874c + 72d9475 commit 2b64e901d21e418ffe55f0c226119d9b17d5226c Pat Patterson committed Feb 29, 2012
View
@@ -17,7 +17,7 @@ There are two mechanisms for installing the toolkit: as an unmanaged package, or
### Installing the Unmanaged Package
1. Create a new Developer Edition (DE) account at http://developer.force.com/join. You will receive an activation email - click the enclosed link to complete setup of your DE environment. This will also log you in to your new DE environment.
-2. Install the unmanaged package into your new DE org via this URL: https://login.salesforce.com/packaging/installPackage.apexp?p0=04td000000019n4
+2. Install the unmanaged package into your new DE org via this URL: https://login.salesforce.com/packaging/installPackage.apexp?p0=04td00000001Hsg
3. Click through the screens to complete installation.
4. Go to **Setup | Administration Setup | Security Controls | Remote Site Settings** and add https://graph.facebook.com as a new remote site.
@@ -42,7 +42,7 @@ There are two mechanisms for installing the toolkit: as an unmanaged package, or
1. Go to **Setup | App Setup | Develop | Sites** and create a new site. Set the home page to `FacebookSamplePage` and add `FacebookTestUser` to the list of Site Visualforce Pages. Ensure you activate the site.
2. Go to **Setup | App Setup | Develop | Apex Classes**, hit the 'Compile All Classes' link, then click 'Schedule Apex' and add `FacebookHousekeeping` - set it to run at midnight every night. This scheduled Apex job will remove expired session records from the FacebookSession__c object.
3. Go to the [Facebook Apps Page](https://developers.facebook.com/apps), click 'Create New App' and complete the required fields. Under 'Website', set Site URL to your site's secure URL - for example, https://fbtest-developer-edition.na14.force.com/
-4. In your DE environment, select the 'Facebook Toolkit 3' app from the application menu at top right, then click the 'Facebook Apps' tab. Create a new Facebook app, copying 'App ID' and 'App Secret' from your new app's settings in Facebook. Set 'Extended Permissions' to a comma-separated list of permissions to allow the sample app to access more data; for example, you might use `read_stream, publish_stream` to allow the app to read and write posts on the user's feed. See the [Facebook Graph API documentation](https://developers.facebook.com/docs/reference/api/permissions/) for a full discussion of permissions.
+4. In your DE environment, select the 'Facebook Toolkit 3' app from the application menu at top right, then click the 'Facebook Apps' tab. Create a new Facebook app, copying 'App ID' from your new app's settings in Facebook. Set 'Permissions' to allow the sample app to access more data; for example, you might use `read_stream, publish_stream` to allow the app to read and write posts on the user's feed. See the [Facebook Graph API documentation](https://developers.facebook.com/docs/reference/api/permissions/) for a full discussion of permissions. Note that, after you save the Facebook App record, you must click the 'Set App Secret' button to enter the 'App Secret' from your new app's settings in Facebook.
5. Go to your site URL (e.g. https://fbtest-developer-edition.na14.force.com/) and you should be prompted to log in to your new app. Do so and you should see a sample page showing your Facebook user name, profile picture, feed, 'Like' button etc. There are buttons to dynamically retrieve your user profile and friends list.
6. Now you have the sample page working, you have a starting point for a Facebook app running on Force.com. Examine `FacebookSamplePage` and `FacebookSampleController` to see how the sample app is put together.
@@ -101,4 +101,10 @@ You can see many similar examples in the sample pages and controllers:
* `FacebookTestUser`
* `FacebookTestUserController`
+### Security Considerations
+
+The toolkit AES-256 encrypts secrets at rest (Facebook application client secrets and user access tokens), dynamically creating a key on first use and saving that key in a protected custom setting. As a result, these secrets are secure when the toolkit is used in a managed package - the key is inaccessible outside the package, and can only be created when a user with the 'Customize Application' permission (for example, a user with the System Administrator profile) creates the first Facebook App record.
+
+Note that, if the toolkit is used outside a managed package, these secrets are accessible to any users that can access the custom setting, either directly in the console, or indirectly via Apex code.
+
For more information, see the [getting started guide](http://wiki.developerforce.com/page/Getting_Started_with_the_Force.com_Toolkit_for_Facebook,_Version_3.0).
@@ -0,0 +1,23 @@
+public with sharing class FacebookAppSecretController {
+ ApexPages.StandardController controller;
+ private FacebookApp__c app;
+ private String secret;
+
+ public FacebookAppSecretController(ApexPages.StandardController controller) {
+ this.controller = controller;
+ this.app = (FacebookApp__c)controller.getRecord();
+ }
+
+ public String getSecret() {
+ return null;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public PageReference save() {
+ app.clientSecret__c = FacebookCrypto.encrypt(secret);
+ return controller.save();
+ }
+}
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
+ <apiVersion>24.0</apiVersion>
+ <status>Active</status>
+</ApexClass>
@@ -0,0 +1,53 @@
+public with sharing class FacebookCrypto {
+ // Throw an exception if user does not have 'Customize Application' perm
+ private static void checkUserCanCustomizeApplication() {
+ User user = [SELECT Id, ProfileId
+ FROM User
+ WHERE Id = :UserInfo.getUserId() LIMIT 1];
+
+ List<Profile> profile = [SELECT Id
+ FROM Profile
+ WHERE Id = :user.ProfileId AND PermissionsCustomizeApplication = true];
+
+ if (profile.size() > 0) {
+ return;
+ }
+
+ List<PermissionSet> permSets =
+ [SELECT Id
+ FROM PermissionSet
+ WHERE PermissionsCustomizeApplication = true AND
+ Id IN (SELECT PermissionSetId
+ FROM PermissionSetAssignment
+ WHERE AssigneeId = :user.Id)];
+
+ if (permSets.size() > 0) {
+ return;
+ }
+
+ throw new FacebookException('User does not have permission to set encryption key');
+ }
+
+ public static String decrypt(String data) {
+ EncryptionSettings__c settings = EncryptionSettings__c.getOrgDefaults();
+ if (settings.key__c == null) {
+ throw new FacebookException('Cannot decrypt without a key!');
+ }
+ Blob key = EncodingUtil.base64Decode(settings.key__c);
+ return Crypto.decryptWithManagedIV('AES256', key, EncodingUtil.base64Decode(data)).toString();
+ }
+
+ public static String encrypt(String data) {
+ EncryptionSettings__c settings = EncryptionSettings__c.getOrgDefaults();
+ Blob key = null;
+ if (settings.key__c == null) {
+ checkUserCanCustomizeApplication();
+ key = Crypto.generateAesKey(256);
+ settings.key__c = EncodingUtil.base64Encode(key);
+ insert settings;
+ } else {
+ key = EncodingUtil.base64Decode(settings.key__c);
+ }
+ return EncodingUtil.base64Encode(Crypto.encryptWithManagedIV('AES256', key, Blob.valueOf(data)));
+ }
+}
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
+ <apiVersion>24.0</apiVersion>
+ <status>Active</status>
+</ApexClass>
@@ -76,7 +76,7 @@ public virtual class FacebookLoginController {
String authuri = 'https://graph.facebook.com/oauth/access_token?client_id='+
fapps[0].clientID__c+'&redirect_uri='+rediruri+exPerms+
- '&client_secret='+fapps[0].clientSecret__c+'&code='+code;
+ '&client_secret='+FacebookCrypto.decrypt(fapps[0].clientSecret__c)+'&code='+code;
System.debug('authuri is:'+authuri);
HttpRequest req = new HttpRequest();
@@ -1,45 +1,45 @@
public with sharing class FacebookToken {
- public static boolean testmode { get; set; }
- public static String cookieName = 'fbsession';
+ public static boolean testmode { get; set; }
+ public static String cookieName = 'fbsession';
- /**
- * Get the access token for the current user
- */
- public static String getAccessToken(){
- if (testmode != null && testmode == true){
- return '';
- } else {
- if (ApexPages.currentPage() == null) {
- throw new FacebookException('ApexPages.currentPage() is null - Facebook API not supported');
- }
- if (ApexPages.currentPage().getCookies().get(cookieName) == null) {
+ /**
+ * Get the access token for the current user
+ */
+ public static String getAccessToken(){
+ if (testmode != null && testmode == true){
+ return '';
+ } else {
+ if (ApexPages.currentPage() == null) {
+ throw new FacebookException('ApexPages.currentPage() is null - Facebook API not supported');
+ }
+ if (ApexPages.currentPage().getCookies().get(cookieName) == null) {
+ return null;
+ }
+ if (ApexPages.currentPage().getParameters().containsKey('code')) {
+ // We're still doing the login flow with Facebook - we've set
+ // the access token, but we need to let the redirect complete
+ // to avoid the "You have uncommitted work pending. Please
+ // commit or rollback before calling out" error
return null;
- }
- if (ApexPages.currentPage().getParameters().containsKey('code')) {
- // We're still doing the login flow with Facebook - we've set
- // the access token, but we need to let the redirect complete
- // to avoid the "You have uncommitted work pending. Please
- // commit or rollback before calling out" error
- return null;
- }
- String sessionid = ApexPages.currentPage().getCookies().get(cookieName).getValue();
-
+ }
+ String sessionid = ApexPages.currentPage().getCookies().get(cookieName).getValue();
+
List<FacebookSession__c> sessions = [SELECT SessionId__c, AccessToken__c
FROM FacebookSession__c
WHERE SessionId__c = :sessionid];
- System.debug('sessionid='+sessionid);
- System.debug('sessions='+sessions.size());
-
- if(sessions.size() == 0 || sessions[0].AccessToken__c == null) {
- return null;
- }
-
- return sessions[0].AccessToken__c;
- }
- }
-
- public static void setAccessToken(String response) {
+ System.debug('sessionid='+sessionid);
+ System.debug('sessions='+sessions.size());
+
+ if(sessions.size() == 0 || sessions[0].AccessToken__c == null) {
+ return null;
+ }
+
+ return FacebookCrypto.decrypt(sessions[0].AccessToken__c);
+ }
+ }
+
+ public static void setAccessToken(String response) {
//response in format of access_token=XXXXX&expires=YYYY
String accessToken = '';
Integer expires = 3600;
@@ -73,10 +73,10 @@ public with sharing class FacebookToken {
}
ApexPages.currentPage().setCookies(new Cookie[]{new Cookie(cookieName,sessionId,null,-1,false)});
- insert new FacebookSession__c(AccessToken__c = accessToken, SessionId__c = sessionId, Expiry__c = DateTime.now().addSeconds(expires));
- }
-
- public static void deleteAccessToken() {
+ insert new FacebookSession__c(AccessToken__c = FacebookCrypto.encrypt(accessToken), SessionId__c = sessionId, Expiry__c = DateTime.now().addSeconds(expires));
+ }
+
+ public static void deleteAccessToken() {
if (ApexPages.currentPage() == null) {
throw new FacebookException('ApexPages.currentPage() is null - Facebook API not supported');
}
@@ -95,22 +95,22 @@ public with sharing class FacebookToken {
if(sessions.size() > 0) {
delete sessions[0];
}
- }
-
-
- /**
- * Facebook provides access tokens for a period of 2hours before expiring them
- */
- private boolean hasTokenExpired(Datetime lastmod) {
- if(lastmod.addHours(2) < DateTime.now())
- {
- System.debug(LoggingLevel.INFO, 'FACEBOOK ACCESS TOKEN IS OK');
- return true;
- }
- else
- {
- System.debug(LoggingLevel.INFO, 'FACEBOOK ACCESS TOKEN HAS EXPIRED');
- return false;
- }
- }
+ }
+
+
+ /**
+ * Facebook provides access tokens for a period of 2hours before expiring them
+ */
+ private boolean hasTokenExpired(Datetime lastmod) {
+ if(lastmod.addHours(2) < DateTime.now())
+ {
+ System.debug(LoggingLevel.INFO, 'FACEBOOK ACCESS TOKEN IS OK');
+ return true;
+ }
+ else
+ {
+ System.debug(LoggingLevel.INFO, 'FACEBOOK ACCESS TOKEN HAS EXPIRED');
+ return false;
+ }
+ }
}
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
+ <customSettingsType>Hierarchy</customSettingsType>
+ <customSettingsVisibility>Protected</customSettingsVisibility>
+ <description>Encryption key for private data. Set on first use.</description>
+ <enableFeeds>false</enableFeeds>
+ <fields>
+ <fullName>Key__c</fullName>
+ <description>Base64 encoded encryption key</description>
+ <externalId>false</externalId>
+ <label>Key</label>
+ <length>255</length>
+ <required>true</required>
+ <type>Text</type>
+ <unique>false</unique>
+ </fields>
+ <label>Encryption Settings</label>
+</CustomObject>
@@ -55,7 +55,7 @@
<inlineHelpText>Also known as Application Secret</inlineHelpText>
<label>App Secret</label>
<length>255</length>
- <required>true</required>
+ <required>false</required>
<trackFeedHistory>false</trackFeedHistory>
<type>Text</type>
<unique>false</unique>
@@ -331,21 +331,22 @@
<label>All</label>
</listViews>
<nameField>
- <label>FacebookApp Name</label>
+ <label>App Name</label>
<trackFeedHistory>false</trackFeedHistory>
<type>Text</type>
</nameField>
<pluralLabel>Facebook Apps</pluralLabel>
<searchLayouts/>
<sharingModel>ReadWrite</sharingModel>
<webLinks>
- <fullName>Authorize</fullName>
+ <fullName>Set_App_Secret</fullName>
<availability>online</availability>
<displayType>button</displayType>
- <linkType>url</linkType>
- <masterLabel>Authorize</masterLabel>
- <openType>replace</openType>
+ <height>600</height>
+ <linkType>page</linkType>
+ <masterLabel>Set App Secret</masterLabel>
+ <openType>sidebar</openType>
+ <page>FacebookAppSecret</page>
<protected>false</protected>
- <url>/apex/FacebookLogin?id={!FacebookApp__c.Id}</url>
</webLinks>
</CustomObject>
@@ -42,7 +42,7 @@
<fullName>AccessToken__c</fullName>
<externalId>false</externalId>
<label>AccessToken</label>
- <length>120</length>
+ <length>255</length>
<required>true</required>
<type>Text</type>
<unique>false</unique>
@@ -0,0 +1,17 @@
+<apex:page standardController="FacebookApp__c" extensions="FacebookAppSecretController">
+ <apex:pageMessages />
+ <apex:form >
+ <apex:pageBlock title="Facebook App Secret">
+ <apex:pageBlockButtons >
+ <apex:commandButton value="Save" action="{!save}"/>
+ <apex:commandButton value="Cancel" action="{!cancel}"/>
+ </apex:pageBlockButtons>
+ <apex:pageBlockSection columns="1">
+ <apex:pageBlockSectionItem >
+ <apex:outputLabel for="secret">App Secret</apex:outputLabel>
+ <apex:inputSecret id="secret" value="{!secret}"/>
+ </apex:pageBlockSectionItem>
+ </apex:pageBlockSection>
+ </apex:pageBlock>
+ </apex:form>
+</apex:page>
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ApexPage xmlns="http://soap.sforce.com/2006/04/metadata">
+ <apiVersion>24.0</apiVersion>
+ <label>FacebookAppSecret</label>
+</ApexPage>
@@ -36,7 +36,6 @@
<br/>
<h2>Sample Page</h2>&nbsp;&nbsp;<apex:outputLink value="FacebookTestUser">Test User Connections</apex:outputLink>
<p>This page shows you how to use the Force.com Toolkit for Facebook.</p>
- <p>Your access token: {!accessToken}</p>
<h2>{!me.name}</h2>
<br/>
@@ -17,7 +17,6 @@
<apex:outputLink value="FacebookSamplePage">Sample Page</apex:outputLink>&nbsp;&nbsp;<h2>Test User Connections</h2>
<p>This page allows you to retrieve fields and connections for a given user. Enter a Facebook user ID (use <b>me</b>
to see your own data) or search term and click the buttons to retrieve data.</p>
- <p>Your access token: {!accessToken}</p>
<!-- A simple Form that calls methods in the Controller, the USER ID is used to call the connections of the user object in the graph api. -->
<apex:pageBlock title="Facebook Toolkit - User Object Tests">

0 comments on commit 2b64e90

Please sign in to comment.