OAuth 1.0 – How to Use Encoded Requests for External Webservice Calls

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.

Best Answer

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

Related Topic