8

I have a business requirement that necessitates a callout from Apex to an external .NET based web service that receives signed OAuth responses. There is no built-in support for this in Apex, but thanks to the internet, I found some code that seems to be able to do this. From https://gist.github.com/surjikal/7539745:

public class Util_OAuth {

public static HttpRequest signRequest(HttpRequest req, String consumerKey, String consumerSecret) {
    String nonce     = String.valueOf(Crypto.getRandomLong());
    String timestamp = String.valueOf(DateTime.now().getTime() / 1000);

    Map<String,String> parameters = new Map<String,String>();
    parameters.put('oauth_signature_method','HMAC-SHA1');
    parameters.put('oauth_consumer_key', consumerKey);
    parameters.put('oauth_timestamp', timestamp);
    parameters.put('oauth_nonce', nonce);

    String signature = generateSignature(req, consumerSecret, parameters);
    String header = generateHeader(signature, parameters);
    req.setHeader('Authorization', header);

    return req;
}

private static String generateHeader(String signature, Map<String,String> parameters) {
    String header = 'OAuth ';
    for (String key : parameters.keySet()) {
        header = header + key + '="'+parameters.get(key)+'", ';
    }
    return header + 'oauth_signature="' + signature + '"';
}

private static String generateSignature(HttpRequest req, String consumerSecret, Map<String,String> parameters) {
    String s = createBaseString(req, parameters);
    Blob sig = Crypto.generateMac(
       'HmacSHA1'
      , Blob.valueOf(s)
      , Blob.valueOf(consumerSecret + '&')
    );
    return EncodingUtil.urlEncode(EncodingUtil.base64encode(sig), 'UTF-8');
}

private static String createBaseString(HttpRequest req, Map<String,String> parameters) {
    Map<String,String> p = parameters.clone();
    if(req.getMethod().equalsIgnoreCase('post') && req.getBody()!=null &&
       req.getHeader('Content-Type')=='application/x-www-form-urlencoded') {
        p.putAll(getUrlParams(req.getBody()));
    }
    String host = req.getEndpoint();
    Integer n = host.indexOf('?');
    if(n>-1) {
        p.putAll(getUrlParams(host.substring(n+1)));
        host = host.substring(0,n);
    }
    List<String> keys = new List<String>();
    keys.addAll(p.keySet());
    keys.sort();
    String s = keys.get(0)+'='+p.get(keys.get(0));
    for(Integer i=1;i<keys.size();i++) {
        s = s + '&' + keys.get(i)+'='+p.get(keys.get(i));
    }

    // According to OAuth spec, host string should be lowercased, but Google and LinkedIn
    // both expect that case is preserved.
    return req.getMethod().toUpperCase()+ '&' +
        EncodingUtil.urlEncode(host, 'UTF-8') + '&' +
        EncodingUtil.urlEncode(s, 'UTF-8');
}

private static Map<String,String> getUrlParams(String value) {
    Map<String,String> res = new Map<String,String>();
    if(value==null || value=='') {
        return res;
    }
    for(String s : value.split('&')) {
        System.debug('getUrlParams: '+s);
        List<String> kv = s.split('=');
        if(kv.size()>1) {
            System.debug('getUrlParams: -> '+kv[0]+','+kv[1]);
            res.put(kv[0],kv[1]);
        }
    }
    return res;
}

}

I have to say that I'm a bit stumped when it comes to actually implementing this. The web service endpoint expects the request url parameters be encoded in the correct way. But I can't get this to work.

I'm aware that this is somewhat lazy, getting ideas how to implement 3rd party code in my work :) But I want to know if anyone has ever worked with OAuth 1.0 in Apex callouts, and gotten it to properly work.

edit: I've gotten mot of the functionality to work, but it seems that the signing just creates null values in the header. Here is my class that does the actual work:

public with sharing class WorkorderBridge {

    public static void Create_Workorder(String woId) {

        FieldServicesB2B__c wo = [SELECT Id, tx_RepairCost__c, pl_Manufacturer__c, 
                                  pl_TypeOfPhoneEquipment__c, ,
                                  Contact__r.Name,Comments__c,
                                  PhoneNumber__c, pl_Fault1__c, tx_IMEINumber__c 
                                  FROM FieldServicesB2B__c
                                  WHERE Id =: woId LIMIT 1 ];





        String content = '{"Test": "Test", "Name":"Test 2","Phone":"'+wo.PhoneNumber__c+'","Name":"'+ wo.Contact__r.Name+'","Description":"'+wo.pl_Fault1__c+'"}'  ;
        System.debug('++++++'+content);
        String endp = 'ENDPOINT_URL';

        String result ='';



        HttpRequest req = new HttpRequest();
        HttpResponse res = new HttpResponse();
        Http http = new Http();

        String ConsumerKey = 'test';
        String ConsumerSecret = 'test';


        req.setEndpoint(endp);
        req.setBody(content);
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
        system.debug('++++*****++++'+req.GetBody());
        req.setMethod('POST');
        req.setCompressed(false);


        req = OAuth.signRequest(req,ConsumerKey,ConsumerSecret);
        String oauth_con_key = req.getHeader('oauth_consumer_key');
        String sig_meth = req.getHeader('oauth_signature_method');
        String timestamp = req.getHeader('oauth_timestamp');
        String nonce = req.getHeader('oauth_nonce');
       // System.debug(req.getBodyAsBlob());



        endp = endp+'?oauth_signature_method='+sig_meth+'&oauth_consumer_key='+oauth_con_key+'&oauth_timestamp='+timestamp+'&oauth_nonce='+nonce;
        System.debug('ENDPOINT URL IS NOW: '+endp);

        req.setEndpoint(endp);
        system.debug('REQUEST ENDPOINT URL IS NOW: '+req.getEndpoint());

        try {
            res = http.send(req);
            System.debug('REQUEST BODY AFTER SEND: '+req.GetBody());
            System.debug('oauth_signature_method= '+req.getHeader('oauth_signature_method'));
            System.debug('oauth_consumer_key= '+req.getHeader('oauth_consumer_key'));
            System.debug('oauth_timestamp= '+req.getHeader('oauth_timestamp'));
             System.debug('oauth_nonce= '+req.getHeader('oauth_nonce'));






        } catch(System.CalloutException e) {
            System.debug('Callout error: '+ e);
            System.debug(res.toString());
            result = res.toString();

        }
         result += 'Success';
         System.debug(res.toString());


         wo.Comments__c += '++++****++++\n\n'+res.toString();
         update wo;






    }
}

When I run this via Anonymous Apex with heaps of logging turned on i get this:

00:04:11.384 (384776000)|METHOD_EXIT|[29]|01p11000000Cno9|OAuth.createBaseString(System.HttpRequest, MAP)
00:04:11.385 (385023000)|METHOD_EXIT|[13]|01p11000000Cno9|OAuth.generateSignature(System.HttpRequest, String, MAP)
00:04:11.385 (385050000)|METHOD_ENTRY|[14]|01p11000000Cno9|OAuth.generateHeader(String, MAP)
00:04:11.385 (385087000)|SYSTEM_CONSTRUCTOR_ENTRY|[22]|()
00:04:11.385 (385107000)|SYSTEM_CONSTRUCTOR_EXIT|[22]|()
00:04:11.385 (385407000)|METHOD_EXIT|[14]|01p11000000Cno9|OAuth.generateHeader(String, MAP)
00:04:11.385 (385466000)|METHOD_EXIT|[40]|01p11000000Cno9|OAuth.signRequest(System.HttpRequest, String, String)
00:04:11.385 (385622000)|USER_DEBUG|[50]|DEBUG|ENDPOINT URL IS NOW: ENDPOINT_URL?oauth_signature_method=null&oauth_consumer_key=null&oauth_timestamp=null&oauth_nonce=null
00:04:11.385 (385705000)|USER_DEBUG|[53]|DEBUG|REQUEST ENDPOINT URL IS NOW: ENDPOINT_URL?oauth_signature_method=null&oauth_consumer_key=null&oauth_timestamp=null&oauth_nonce=null
00:04:11.385 (385815000)|CALLOUT_REQUEST|[56]|System.HttpRequest[Endpoint= ENDPOINT_URL?oauth_signature_method=null&oauth_consumer_key=null&oauth_timestamp=null&oauth_nonce=null, Method=POST]
00:04:11.794 (794696000)|CALLOUT_RESPONSE|[56]|System.HttpResponse[Status=Bad Request, StatusCode=400]
00:04:11.794 (794984000)|USER_DEBUG|[57]|DEBUG|REQUEST BODY AFTER SEND: {"Test": "Test", "Name":"Test 2","Phone":"'+wo.PhoneNumber__c+'","Name":"'+ wo.Contact__r.Name+'","Description":"'+wo.pl_Fault1__c+'"}
00:04:11.795 (795083000)|USER_DEBUG|[58]|DEBUG|oauth_signature_method= null
00:04:11.795 (795139000)|USER_DEBUG|[59]|DEBUG|oauth_consumer_key= null
00:04:11.795 (795188000)|USER_DEBUG|[60]|DEBUG|oauth_timestamp= null
00:04:11.795 (795237000)|USER_DEBUG|[61]|DEBUG|oauth_nonce= null
00:04:11.795 (795300000)|USER_DEBUG|[75]|DEBUG|System.HttpResponse[Status=Bad Request, StatusCode=400]

This endpoint expects the parameters to be encoded into the URL and not just sent as a part of the header. All the header values are just 'null'. How can I call the headers so i can see the values of them all before sending the request to the endpoint url?

UPDATE 3-1 : I've gotten that far with this that the only thing stumping me now is that the oauth_signature is calculated incorrectly. The relevant code is this here:

private static String generateSignature(HttpRequest req, String consumerSecret, Map<String,String> parameters) {
        String s = createBaseString(req, parameters);
        s = EncodingUtil.urlEncode(s, 'UTF-8');
        System.debug('INSIDE generateSignature: String s is: '+ s);
        String s2 = req.getMethod() + '&' + EncodingUtil.urlEncode(req.getEndpoint(), 'UTF-8')  + '&' + s;
        System.debug('INSIDE generateSignature: String s2 is: '+ s2);

        Blob sig = Crypto.generateMac(
           'HmacSHA1'
          , Blob.valueOf(s2)
          , Blob.valueOf(consumerSecret+ '&')
         );
        return EncodingUtil.urlEncode(EncodingUtil.base64encode(sig), 'UTF-8');

I'm outputting the strings to the debug log, and as far as I can see, this all conforms to the Oauth 1.0 signature spec as laid out here. I'm just wondering about the blob creation (the usage of the Crypto class), that's the only thing I'm not too sure on right now. According to the spec it should be be calculated with the HMAC-SHA1 signature method, but is it sufficient to use only the parameter string along with the consumer secret to encode this? I'm getting errors on the server side, that indicate that my signature base is off. I've gone over it dozens of times and the base string that's being passed to the Crypto class is what the server expects. But yet the server calculates it as off at post time.

2
  • 1
    Question title says OAuth 2.0, body says OAuth 1.0. Which are you trying to use?
    – metadaddy
    Jan 28, 2014 at 23:50
  • Sorry, this is OAuth 1.0, updated accordingly
    – akarnid
    Jan 29, 2014 at 12:29

2 Answers 2

3

It looks like you're using an old version of Jesper Joergensen's OAuth Playground. I worked on this a couple of years ago to get it working with a variety of providers, and needed to fix parameter encoding.

The relevant section is:

private Map<String,String> getUrlParams(String value) {
    Map<String,String> res = new Map<String,String>();
    if(value==null || value=='') {
        return res;
    }
    for(String s : value.split('&')) {
        System.debug('getUrlParams: '+s);
        List<String> kv = s.split('=');
        if(kv.size()>1) {
            // RFC 5849 section 3.4.1.3.1 and 3.4.1.3.2 specify that parameter names 
            // and values are decoded then encoded before being sorted and concatenated
            // Section 3.6 specifies that space must be encoded as %20 and not +
            String encName = EncodingUtil.urlEncode(EncodingUtil.urlDecode(kv[0], 'UTF-8'), 'UTF-8').replace('+','%20');
            String encValue = EncodingUtil.urlEncode(EncodingUtil.urlDecode(kv[1], 'UTF-8'), 'UTF-8').replace('+','%20');
            System.debug('getUrlParams:  -> '+encName+','+encValue);
            res.put(encName,encValue);
        }
    }
    return res;
}

My fork is at https://github.com/metadaddy-sfdc/sfdc-oauth-playground

7
  • Thanks metadaddy. I tried this with a new version of the class in a fresh sandbox and heard that the same error is still occurring on the endpoint server. I'm trying to set debug messages as I go along, but I'm getting nothing of interest in my debug logs when I try to execute this code in Anonymous Apex.
    – akarnid
    Jan 30, 2014 at 15:35
  • What error is the server reporting?
    – metadaddy
    Jan 30, 2014 at 18:54
  • 400 Bad Request. I'll probably have to reach out to the server admins and have them send me my request.
    – akarnid
    Jan 30, 2014 at 19:30
  • The problem is that the server expects the header parameters be sent as URL parameters. I see when debugging that the values are set in the header but when I set the 'Content-Type' as 'application/x-www-form-urlencoded' there is nothing appended or put into the endpoint URL.
    – akarnid
    Feb 12, 2014 at 19:27
  • What I'm battling now is the fact that all OAuth related headers in the request seem to just null values. And I don't really understand why.
    – akarnid
    Feb 13, 2014 at 0:10
0

I found my answer finally. By reading here over how to construct the Signature Base I finally got it to work. I'm considering this solved as is. But anyone is free to chime in and I'd like to let this thread stand as a starting point if anyone else runs into the same kind of problem.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .