Dedupe with Metaphone and Google Maps

Litmus Test to Identify Duplicates

Have you bought tool to identify,remove or prevent duplicate contacts, leads etc?
This is how you can test if it is worth it’s money.

Enter two contacts:

1. Marc Foreman, 139 E. Lancaster Ave. Radnor PA.

and

2. Mark Phoreman, 139 East Lancaster Avenue, Wayne Pennsylvania.

Now enter duplication criteria as Name fuzzy match with distance less than 3 and Address exact match.

Does your tool identify these two contacts as probable duplicate? If yes, then congratulations, You have bought the right tool.

If not, try using free but very powerful tools available: Metaphone and Google Maps API.

But why are these records duplicates? Names are sounding similar but addresses are different. But are they?

Only map APIs such as Google Map can answer this question.

First let us see how Names are identified as similar.

Metaphone

Metaphone is a phonetic algorithm which creates metaphonic code for English words. In my previous Blog I have explained how Soundex algorithm can be used to perform phonetic search or match by indexing with Soundex code. Similarly Metaphone can be used to index and match words phonetically.  Soundex uses first alphabet  to generate Soundex code which is a major impedement. e.g. in this case Phoreman has a Soundex code P650 while Foreman has F650. Thus Soundex will never identify them as phonetically alike. On other hand Metaphone correctly generates code as MRK for Mark and Marc while FRMN for Foreman and Phoreman.

We can also use  Apex getLevenshteinDistance string method to calculate distance between the two strings. In our case it is 2 for the last name while 1 for the first name.

Google Maps API

Now let use perform Address matching with Google API.

Google Maps API is free as long as it is also used to display records on Map. It can also be used to validate and correct addresses, geocoding and reverse geocoding.

Let us normalize address 139  E. Lancaster Ave. Radnor PA as follows:

http://maps.googleapis.com/maps/api/geocode/xml?address=139+Lancaster+Ave,+radnor+PA&sensor=false

It gives following XML response:

</pre>
<pre id="line1"><?xml version="1.0" encoding="UTF-8"?>
<GeocodeResponse>
 <status>OK</status>
 <result>
  <type>street_address</type>
  <formatted_address>139 East Lancaster Avenue, Wayne, PA 19087, USA</formatted_address>
  <address_component>
   <long_name>139</long_name>
   <short_name>139</short_name>
   <type>street_number</type>
  </address_component>
  <address_component>
   <long_name>East Lancaster Avenue</long_name>
   <short_name>E Lancaster Ave</short_name>
   <type>route</type>
  </address_component>
  <address_component>
   <long_name>Wayne</long_name>
   <short_name>Wayne</short_name>
   <type>locality</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>Radnor</long_name>
   <short_name>Radnor</short_name>
   <type>administrative_area_level_3</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>Delaware</long_name>
   <short_name>Delaware</short_name>
   <type>administrative_area_level_2</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>Pennsylvania</long_name>
   <short_name>PA</short_name>
   <type>administrative_area_level_1</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>United States</long_name>
   <short_name>US</short_name>
   <type>country</type>
   <type>political</type>
  </address_component>
  <address_component>
   <long_name>19087</long_name>
   <short_name>19087</short_name>
   <type>postal_code</type>
  </address_component>
  <geometry>
   <location>
    <lat>40.0440030</lat>
    <lng>-75.3864640</lng>
   </location>
   <location_type>ROOFTOP</location_type>
   <viewport>
    <southwest>
     <lat>40.0426540</lat>
     <lng>-75.3878130</lng>
    </southwest>
    <northeast>
     <lat>40.0453520</lat>
     <lng>-75.3851150</lng>
    </northeast>
   </viewport>
  </geometry>
 </result>
</GeocodeResponse>

Both the addresses get exactly same response.

Google has not only corrected the address but provided lot more information such as latitude, longitude,street number,zip code etc. This is very important information and can be used for location analytic. But for now we are going to use formatted address which is 139 East Lancaster Avenue, Wayne, PA 19087, USA  for both the records. Thus by using Google Maps API and Metaphone algorithm in tandem we can deduce that these two records are most probably duplicates and need to be mitigated. We have also managed to validate and clean our addresses.

Conclusion

As old saying goes: Best Things in Life are FREE.

Phonetic Search for duplicates in Salesforce.com

Soundex Search Screen

Introduction
Phonetic search is basically searching for terms which may be spelled differently but have similar pronunciation.
Though phonetic search  is quite common requirement, searches are often implemented in term of an exact match criterion with wildcards. I think if there is any application which must have phonetic search then it is CRM. Why?
Because CRM may contain contacts and leads with homo phonic (pronounced the same) names. Also, most of the times the data is manually entered by humans who are well known for the propensity to misspell. This creates a situation where duplicate contacts or leads are entered because there names are misspelled or a contact is not found because it is being searched for a misspelled name such as Smith instead of Smyth. As far as I know, Salesforce has neither implemented any phonetic search capability nor made any announcement to do so. But I am sure someday they will and for time being I have decided to implement it by using Soundex algorithm as shown above. I am searching for last name Branscomb and it has returned 4 records with Last names sounding like Branscomb.

Soundex Algorithm
Soundex algorithm is one of the simplest algorithms to implement. It was developed by Robert Russell and Margaret Odell in 1918. It generates four letter term (Alphabet+Three digits such as A230) for a given word called soundex code by applying following rules:

1. First letter of the word is the letter of the soundex code and not converted to a digit (number).  So for a Last name Smith it is “S”.

2.  Vowels are Not coded.

3.  b,f,p,v are converted to 1.

4. c, g, j, k, q, s, x, z are converted to 2.

5. d,t are converted to 3

6. l is converted to 4.

7. m,n are converted to 5.

8. r is converted to 6.

9 h,w are Not coded

10. Two adjacent letters with the same number are coded as a single number.
Letters with the same number separated by an h or w are also coded as a single number.

11. Continue until you have one letter and three numbers.
If length of the soundex code is less than 4, fill in zeroes until there are three numbers.

By applying these rules, Smith and Smyth both yield to soundex code S530.  Thus if you search for S530, both Smith and Smyth will be returned.

Though simplistic, soundex has limitations.

Design and Implementation

Salesforce design will be very straight forward and very similar to my last blog post to prevent duplicates.

Suppose we need to implement phonetic search for Lastname field.

1. A new custom field of type text and length 4 called LastNameSoundexKey__c will be defined.

2. Make sure that LastNameSoundexKey__c is defined as External Id. This way it is guaranteed to be  indexed.

3. Write an Apex class with a static method to convert any String to a Soundex Code.

4. Write a before insert/update trigger code to convert Lastname to soundex code and assign to LastNameSoundexKey__c.

5. Develop a simple Visual Force page as shown above where user may enter search term and a controller will convert the search term to a soundex code and make a SOQL query.  Thus if user is searching for Smith, controller will actually search for S530.

Code Snippet

Soundex Class:

/*
Copyright (c) 2012, Ardent Software, LLC.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Ardent Software, LLC. nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

****

Note that this code may require some work before you can deploy it to a standard org.
*/
public class Soundex{
public static integer soundexLength = 4;

private static map<String,String> soundexMap = new map<String,String>{'A'=>'0','B'=>'1','C'=>'2','D'=>'3','E'=>'0',
'F'=>'1','G'=>'2','H'=>'0','I'=>'0','J'=>'2',
'K'=>'2','L'=>'4','M'=>'5','N'=>'5','O'=>'0',
'P'=>'1','Q'=>'2','R'=>'6','S'=>'2','T'=>'3',
'U'=>'0','V'=>'1','W'=>'0','X'=>'2','Y'=>'0','Z'=>'2'};
public static string toSoundex(String input){
String prevChar = ' ';
if (input == NULL || input.length() == 0){
return input;
}
String normStr = input.toUpperCase();
//Append first character to encoded string
String soundexStr = normStr.substring(0,1);
integer strLength = normStr.length();
for (integer i=1; i<strLength && soundexStr.length()<soundexLength; i++){
String key=normStr.substring(i,i+1);
String soundexChar = soundexMap.get(key);

if (soundexChar != NULL && !soundexChar.equals('0') && !soundexChar.equals(prevChar)){
soundexStr = soundexStr+soundexChar;
prevChar = soundexChar;
}
}
//Pad soundex string if the length is less than 4
while (soundexStr.length() < soundexLength){
soundexStr = soundexStr+'0';
}
return soundexStr;
}
}

Trigger code:

trigger updSoundex on Lead (before insert, before update) {
for (Lead l:Trigger.New){
l.LastNameSoundexKey__c = Soundex.toSoundex(l.LastName);
}

Controller Methods:

public void setQuery(){

generatedKey = Soundex.toSoundex(lastName);

doQuery();
}

public PageReference doQuery(){
leads = [Select l.Lastname
From Lead l
where l.LastNameSoundexKey__c = :generatedKey
ORDER BY l.Lastname ASC];
return null;
}

Soundex class can also be used to flag probable duplicates and merge them.

Preventing Duplicate Records in Salesforce.com

Duplicate Leads, Contacts and Accounts is a major issue hampering quality and productivity of Salesforce.com data. System Administrators would love to handle it by making configuration changes such as making a field Unique and Required. But unfortunately this is not that easy. It is very rare that a record is duplicate based on a single custom field (Unique and Required properties are not available for standard fields). Many organizations choose to allow duplicates and remove later by some kind of deduping process.
The Solution I am proposing is based on a real life situation where the client company had millions of contacts and accounts. Uniqueness was based on a combination of multiple fields such as First Name, Last  Name, State, Country and Zip Code for example.  Their developers had written Apex code in before trigger to select all the records which match these fields during insert or update and if the match is found then prevent the record from saving by throwing an error. This worked quite well on a test sandbox. But once deployed in production the performance took a heavy toll and the code was removed from the trigger logic.  I was asked to look into the issue. The solution was simple and worked quite well without any noticeable performance decadence.

A custom text field called UniqueKey long enough to hold concatenated string of First Name, Last Name, State, Country and Zip was defined. The field was made Unique and Required. Trigger logic (before insert/update) was added to normalize these fields by converting  each field to lower case and removing extra spaces and padding if any.  All these fields were then concatenated and assigned to UniqueKey. And that’s it. Salesforce prevented duplicates very efficiently and effectively.

Obviously a one time batch process was written to populate UniqueKey for the existing records.

Web2Lead an Invitation to Spam?

Salesforce.com provides a very useful feature called Web2Lead to enable Lead generation via a web page.
When you enable Web2Lead, Salesforce generates an HTML code as follows:

<!--  ----------------------------------------------------------------------  -->
<!--  NOTE: Please add the following <META> element to your page <HEAD>.      -->
<!--  If necessary, please modify the charset parameter to specify the        -->
<!--  character set of your HTML page.                                        -->
<!--  ----------------------------------------------------------------------  -->

<META HTTP-EQUIV="Content-type" CONTENT="text/html; charset=UTF-8">

<!--  ----------------------------------------------------------------------  -->
<!--  NOTE: Please add the following <FORM> element to your page.             -->
<!--  ----------------------------------------------------------------------  -->

<form action="https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8" method="POST">

<input type=hidden name="oid" value="<Your_Org_Id>">
<input type=hidden name="retURL" value="http://www.salesforce.com">

<!--  ----------------------------------------------------------------------  -->
<!--  NOTE: These fields are optional debugging elements. Please uncomment    -->
<!--  these lines if you wish to test in debug mode.                          -->
<!--  <input type="hidden" name="debug" value=1>                              -->
<!--  <input type="hidden" name="debugEmail"                                  -->
<!--  value="agaikwad@yahoo.com">                                    -->
<!--  ----------------------------------------------------------------------  -->

<label for="first_name">First Name</label><input  id="first_name" maxlength="40" name="first_name" size="20" type="text" /><br>

<label for="last_name">Last Name</label><input  id="last_name" maxlength="80" name="last_name" size="20" type="text" /><br>

<label for="email">Email</label><input  id="email" maxlength="80" name="email" size="20" type="text" /><br>

<label for="company">Company</label><input  id="company" maxlength="40" name="company" size="20" type="text" /><br>

<label for="city">City</label><input  id="city" maxlength="40" name="city" size="20" type="text" /><br>

<label for="state">State/Province</label><input  id="state" maxlength="20" name="state" size="20" type="text" /><br>

<input type="submit" name="submit">

</form>

This is an HTTP Post Request sent to servlet having a URL https://www.salesforce.com/servlet/servlet.WebToLead.
Notice that there is no authentication performed. So how does this servlet know that the user inserting a
lead is authorized to do so?
In fact while investigating a problem of unexpected leads appearing in Lead object with unexpected owner id,
I noticed that just knowing an Organization Id is enough to insert a lead.
So how does it pose a security problem?
Imagine that a disgruntled ex employee who knows the Organization Id. If he or she decides to play mischief,
he can write a very simple script which sends HTTP post request to insert millions of phony leads.
How to prevent this?
Obviously one of the request parameters must include some token which is validated before inserting the lead.
This token must be stored within salesforce.com and configurable by the System Admin.
Since only salesforce.com internal team has access to servlet code, only they can make this change.

Developing OAuth 2.0 Library with Apex – Day 3

This is the third and final part of the series Developing OAuth 2.0 Library with Apex. After this one should be ready to actually make calls to any 2.0 compliant provider such as Facebook, Klout ,Dropbox and many more.

If you recall, OAuth authorization is a 2 step process and needs a user interface or a visual force page to perform the actual authorization.
The source code of the page is as follows:

<apex:page controller="OAuthController" action="{!finalAuth}">
<apex:form >
<apex:pageBlock title="Social Application Authorization ">
   <apex:pageBlockSection columns="1">
    <apex:pageBlockSectionItem >
       <apex:outputText value="Application"/>
       <apex:selectList value="{!service}" size="1">
         <apex:selectOptions value="{!serviceNames}"/>
       </apex:selectList>
    </apex:pageBlockSectionItem>
    <apex:pageBlockSectionItem >
      <apex:outputText rendered="{!message!=null}" value="{!message}"/>
    </apex:pageBlockSectionItem>
     <apex:commandLink action="{!doAuthorization}" value="Authorize" id="lnCommandLink" target="_blank" />
  </apex:pageBlockSection>
 </apex:pageBlock>
</apex:form>
</apex:page>

The controller code is as follows:

public class OAuthController {
public String service { get; set; }
//Services Picklist
String message=null;
private static List<selectOption> VIEW_SERVICES = new SelectOption[]{
new SelectOption('LinkedIn','LinkedIn'),
new SelectOption('Facebook','Facebook'),
new SelectOption('Twitter','Twitter')
};
public List<SelectOption> getServiceNames(){
return VIEW_SERVICES;
}
public String getMessage(){
return message;
}
public PageReference finalAuth(){
//OAuth_1_0 oa = new OAuth_1_0('Twitter');
String oauth_token = ApexPages.currentPage().getParameters().get('oauth_token');
String oauth_verifier = ApexPages.currentPage().getParameters().get('oauth_verifier');
String oauth_code = ApexPages.currentPage().getParameters().get('code');

//message=oauth_token+' '+oauth_verifier;
//Now call auth service to get authorized tokens
if (oauth_code != null){
    oauth_code = oauth_code+'#_=_'; //Facebook anomaly?
    UserTokens__c userToken = [select Id,OAuth_Service__c,Token__c,secretToken__c from UserTokens__c
                              where isAuthorized__c = false and CreatedBy.Name=:userInfo.getName()];
    OAuth_Service__c oservice = [select Name,AccessTokenUrl__c from OAuth_Service__c
                                where Id = :userToken.OAuth_Service__c LIMIT 1];
    OAuth_2_0 oa = new OAuth_2_0(oservice.Name);
    String authorizedStr=oa.getAuthorizedReqToken(oauth_code);
    String authUserToken=authorizedStr.split('&')[0].split('=')[1];
    //String authUserToken=authorizedStr;
    userToken.Token__c = authUserToken;
    userToken.isAuthorized__c = true;
    upsert userToken;
    message='Authorization Successful..'+'Access Token:'+authUserToken;
}
return null;
}
public PageReference doAuthorization(){
//This is a 2 Legged authorization
// Get UnAuthorized Tokens
System.debug('========================== Service:'+service);
PageReference authPage;
OAuth_Service__c os = [select Id,Name,OAuth_Version__c,ReqTokenUrl__c,AuthUrl__c from OAuth_Service__c where Name = :service];
if (os.OAuth_Version__c != NULL && os.OAuth_Version__c.equalsIgnoreCase('2.0')){
    List<UserTokens__c> unauthorizedTokenList=[select Id from UserTokens__c
                                               where CreatedBy.Name=:userInfo.getName()
                                               and OAuth_Service__c = :os.Id];
    delete unauthorizedTokenList;
    OAuth_2_0 oa = new OAuth_2_0(service);
    UserTokens__c userToken = new UserTokens__c();
    userToken.Name = service+' '+userInfo.getName();
    userToken.OAuth_Service__c = os.Id;
    userToken.isAuthorized__c = false;
    insert userToken;
    String authURL = oa.getUnauthorizedReqToken();
    authPage=new PageReference(authURL);
    authPage.setRedirect(true);
}
return authPage;
}
}

I have also created a TAB called “Authorize Social Media”.
This is how VF page and the whole authorization process works:
STEP 1:

oauth1

I selected Facebook and clicked on Authorize Link. Facebook server is sent an HTTP request to send temporary token so that once the user confirms, it can send a final Token.

STEP 2:

If you are already logged in to Facebook, It will grant authorization immediately else it will show you the following screen:

oAuth2

Note that it is showing the name of my App which is fb2SF (Facebook to Salesforce).

Once you logged in successfully it will create a permanent token and return to Visualforce Page (Since it is a call back URL) as follows:

oAuth4

Actual Token is masked for security reasons. This token is now saved in Salesforce for this user. The token MUST be securely guarded and Object permissions must be managed properly. Until the Authorization is revoked, this token will be used to perform Facebook operations via API.

Object Definition to store access tokens for the user is as follows:

Object is called UserTokens__C and the fields are:

isAuthorized__c           Checkbox,
OAuth_Service__c     Master-Detail(OAuth_Service),
secretToken__c           Text(255),
Token__c                          Text(255)

This concludes the authorization part. If you think I need to write how to make actual meaningful API calls, please let me know via comments.

Developing OAuth 2.0 Library with Apex – Day 2

oauth2_flow
Let us continue to develop our Apex library for oAuth 2.0. The general flow is as follows:
The abstract flow illustrated in the figure above describes the interaction
between the four roles and includes the following steps:
(A) The client which is an Apex class requests authorization from the resource owner who is a
Salesforce user with a valid facebook account.
The authorization request can be made directly to the resource owner
(as shown), or preferably indirectly via an intermediary such as
an authorization server.
(B) The Apex Class receives an intermediate authorization grant which represents the
authorization provided by the resource owner. The authorization
grant type depends on the method used by the client and
supported by the authorization server to obtain it.
(C) The Apex Class requests an access token by authenticating with the
authorization server using its client credentials (such as
Consumer Id and Consumer Secret Key) and presenting the
authorization grant.
(D) Facebook authorization server validates the client credentials and
the authorization grant, and if valid issues an access token.
(E) The client requests the protected resource from the resource
server and authenticates by presenting the access token.
(F) The resource server validates the access token, and if valid,
serves the request.

Let us write an apex class called OAuth_2_0.cls with a constructor to initialize the required class members such as consumer key, consumer secret key etc. from previously defined object.
We will also write methods to perform steps mentioned above  viz.  getUnauthorizedReqToken(), getAuthorizedToken(), makeAPIcall() etc. Complete listing of this class is as follows:

/*
Copyright (c) 2012, Ardent Software, LLC.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the Ardent Software, LLC. nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

****
Note that this code may require some work before you can deploy it to a standard org.
*/
 public class OAuth_2_0{
private String OAUTH_CONSUMER_KEY;
private String OAUTH_CONSUMER_SECRET;
public static final String OAUTH_SIG_METHOD='HMAC-SHA1'; //Required for OAuth 1.0
public static final String OAUTH_VERSION='2.0';
public String OAUTH_NONCE;
public String API_URL = '';
public String AUTH_URL = '';
public String ACCESS_TOKEN_URL = '';
public String OAUTH_CALLBACK_URL;
public String serviceId;
public String serviceName;
private String clientId;
private String clientSecret;
//Constructor.
//Accepts Service name as a parameter and populates consumer key, consumer secret and call back url.
public OAuth_2_0(String serviceName){
 OAuth_Service__c authServ = [select Id,Name,AccessTokenUrl__c,AuthUrl__c,CallBackURL__c,ConsumerKey__c,
                             ConsumerSecretKey__c,ReqTokenUrl__c from OAuth_Service__c where Name = :serviceName LIMIT 1];
 this.clientId = authServ.ConsumerKey__c;
 this.clientSecret = authServ.ConsumerSecretKey__c;
 this.OAUTH_CALLBACK_URL = authServ.CallBackURL__c;
 this.AUTH_URL = authServ.ReqTokenUrl__c;
 this.ACCESS_TOKEN_URL = authServ.AccessTokenUrl__c;
 this.serviceId = authServ.Id;
 this.serviceName = serviceName;
}

//This method creates and returns a URL to get a temporary token
public String getUnauthorizedReqToken(){

  String vf_url=this.OAUTH_CALLBACK_URL; //Visual Force Page used for Authentication
  String state = getRandomNonce(); //Random String
  String body='client_id='+this.clientId+'&redirect_uri='+this.OAUTH_CALLBACK_URL+'&state='+state;

  String retUrl=this.AUTH_URL+'?'+body;
  return retUrl;
}
//Generates Nonce Randomly
public static String getRandomNonce(){
   String allChars='ABCDEFGHIJKLMNOPQRSTUVWXYZ';
   String nonce='';
   for(integer cnt=0; cnt<=9;cnt++)   {
       Integer i = 1+Math.Round(700.0*Math.random()/26);
       if (i<=26)
       {
          i--;
          String newStr=allChars.substring(i,i+1);
          nonce=nonce+newStr;
      }else{
        cnt--;
      }
  }
  return nonce;
}

//This Method get Authorized Token
public String getAuthorizedReqToken(String code){

String body='client_id='+this.clientId+'&redirect_uri='+this.OAUTH_CALLBACK_URL+'&client_secret='+this.clientSecret+'&code='+code;

String tokenResponse = basicAuthCallout(this.ACCESS_TOKEN_URL,'',body,'GET');

System.debug('=========================== RESPONSE:'+tokenResponse);

String authReqToken = tokenResponse;
return authReqToken;
}

//This method makes API calls
//e.g. https://graph.facebook.com/me?access_token=YOUR_USER_ACCESS_TOKEN
public String makeAPICall(String apiUrl,Map<String,String> paramMap){
Long milliTime=System.currentTimeMillis();
String params = getSortedParams(paramMap); //Sort parametrs in ascending order
milliTime=milliTime/1000;
Integer secTime = (Integer)milliTime;
String strTime = String.valueOf(secTime);
String token=null; //Member token
String secretToken=null;//Member secret token
UserTokens__c userToken = [select Id,OAuth_Service__c,Token__c,secretToken__c from UserTokens__c where isAuthorized__c = true
and CreatedBy.Name=:userInfo.getName() and OAuth_Service__c = :this.serviceId];
token = userToken.Token__c;
String body='access_token='+token+'&'+params;

String qryResponse = basicAuthCallout(apiURL,'',body,'GET');
System.debug('====================== QRY RESP:'+qryResponse);
return qryResponse;
}

//Sort the map to return sorted parameters
public static String getSortedParams(Map<String,String> paramMap){
String sortedParams='';
List<String> keys = new List<String>(paramMap.keySet());
keys.sort();
for(String key : keys){
String value=paramMap.get(key);
if (sortedParams == '')
sortedParams=key+'='+value;
else
sortedParams=sortedParams+'&'+key+'='+value;
}
return sortedParams;
}

public String basicAuthCallout(String endPoint,String header, String body,String method){
HttpRequest req = new HttpRequest();
//endPoint = EncodingUtil.urlEncode(endPoint,'UTF-8');
endpoint=endpoint+'?'+body;
req.setEndpoint(endPoint);
req.setMethod(method);
Http http = new Http();
System.debug('=========== REQ BODY:'+endPoint);
HTTPResponse res = http.send(req);
return res.getBody();
}
}

This APEX class is a foundation for not just Authentication but also for Making subsequent API calls.
Details of the Visual Force Page, it’s controller and design of an Object to save user access tokens will
be discussed in my next post.

(more…)

Developing OAuth 2.0 Library with Apex – Day 1

Before diving into actual development of code let us revise what OAuth is and what is the difference between version 1.0 and 2.0.

OAuth stands for Open Authorization and was released in 2007. Since it’s release, it has become a mainstay in web services and social sites arena. Led by giants such as Facebook and Twitter, adoption of open authorization has spread like a fire. At its core OAuth is based on simplicity to the users but nightmare for the developers. It allows users to authenticate 3rd party apps without disclosing their passwords. Users can always revoke the authentication. Thus any application can invoke other application’s API which is mostly RESTful web service returning XML or JSON. But in order to make invocation successful, the calling application must have been registered with the service it is calling and send the required tokens and digital signature (version 1.0) with the request.

Debate over OAuth 2.0

Version 2.0 specification does not require digital signature at all. This has made the life of developers much easier but has compromised security (in my view) and caused a huge controversy. It does require SSL to secure tokens being sent over network though.  Personally I prefer 1.0 for it’s security.

Now let’s start developing for version 2.0.

Initial steps and house keeping

As mentioned earlier, we must register with the service we intend to invoke. Facebook uses version 2.0 while Twitter and LinkedIn are based on version 1.0. Let us start with Facebook and register at https://developers.facebook.com/apps.

Once the app is registered with Facebook, Facebook creates unique App Id/Consumer Key and Consumer Secret Key. We need to save the consumer key and consumer secret key somewhere along with some other information such as URLs to authorize, authenticate and make API calls.  I am going to create a custom object to store this important information. Custom object will be called OAuth_Service__c with following fields and corresponding values:

Service Name : Facebook,
AccessTokenUrl__c Text(255) : https://graph.facebook.com/oauth/access_token, //provided by facebook
AuthUrl__c Text(255),  //Used for version 1.0
CallBackURL__c Text(255): https://c.na2.visual.force.com/apex/Authservices, //VF page.
ConsumerKey__c Text(255): 981657716937450, //Fictitious but provided by Facebook in real life
ConsumerSecretKey__c Text(255): bb756kkk674s7709d50998c7, //Fictitious but provided by Facebook in real life
ReqTokenUrl__c Text(255) : https://www.facebook.com/dialog/oauth, //provided by facebook
OAuth_Version__c Text(10) : 2.0

We will discuss the use of call back URL and visual force page in details while looking into the authentication flow in the next blog.

If interested follow my twits for alerts.

Twitter: @gaikwad_arun

Follow

Get every new post delivered to your Inbox.

Join 46 other followers