Twitter new DM API, legacy method of authorization not working with json request body

restapi
authorization
error-codes
oauth

#1

We are trying to implement Twitter new DM API from Salesforce. We are sending our the JSON request in the body as mentioned in documentation but the legacy method for Oauth authorization is not working. Any help is greatly appreciated.

To add, I am sending a DM from salesforce to twitter, So 1) I am setting the request body in JSON. 2) I am doing a POST. 3) I am hitting the endpoint at ‘https://api.twitter.com/1.1/direct_messages/events/new.json’ 4) Oauth2, getting the access token(successfully) 5) Setting header as (‘Content-Type’, ‘application/json’). 6) Creating Authorization header as twitter mentions using consumer key, Nonce, Signature, Signature method, Timestamp, Version. Building the same as in “Guide” section of developer.twitter.com/en/docs/basics/authentication/guides/ 7) On running the error code “{“errors”:[{“code”:32,“message”:“Could not authenticate you.”}]}”.

Another important information that I had been using twitter old API to send DM that works perfect, only difference is it sends the request body in URL parameters instead of JSOn body but the authorization method remains same. As some new Functionality can only be achieved via Twitter New API and according to documentation the body needs to be sent via JSON format. Therefore the request part is changed but authorization is same.

Code:

String accTok = 'redacted';
String conKey = 'redacted';
String conSec = 'redacted';
String accTokSec = 'redacted';

String theTweet = 'Hello world!';
String screenName ='some_test_username';
String jsonString = TwitterJsonReqGenerator.generateJSON(theTweet, screenName);
system.debug('JSON string ='+jsonString);

httpRequest newReq = new httpRequest();
newReq.setBody(jsonString);
newReq.setMethod('POST');
newReq.setEndpoint('https://api.twitter.com/1.1/direct_messages/events/new.json');

//Generate Nonce
string oAuth_nonce = EncodingUtil.base64Encode(blob.valueOf(string.valueOf(Crypto.getRandomInteger()+system.now().getTime())+string.valueOf(Crypto.getRandomInteger()))).replaceAll('[^a-z^A-Z^0-9]','');

map<String, String> heads = new map<String, String>{
    'oauth_token'=>accTok,
    'oauth_version'=>'1.0',
    'oauth_nonce'=>oAuth_nonce,
    'oauth_consumer_key'=>conKey,
    'oauth_signature_method'=>'HMAC-SHA1',
    'oauth_timestamp'=>string.valueOf(system.now().getTime()/1000)
};

//Alphabetize 
string[] paramHeads = new string[]{};
paramHeads.addAll(heads.keySet());
paramHeads.sort();
string params = '';
for(String encodedKey : paramHeads){
    params+=encodedKey+'%3D'+heads.get(encodedKey)+'%26';
}

//params+='status'+percentEncode('='+percentEncode(theTweet));
params+=percentEncode(theTweet);

//Build the base string
string sigBaseString = newReq.getMethod().toUpperCase()+'&'+EncodingUtil.urlEncode(newReq.getEndpoint(),'UTF-8')+'&'+params;
system.debug('signatureBaseString == '+sigBaseString);

//calculate signature
string sigKey = EncodingUtil.urlEncode(conSec,'UTF-8')+'&'+EncodingUtil.urlEncode(accTokSec,'UTF-8');
blob mac = crypto.generateMac('hmacSHA1', blob.valueOf(sigBaseString), blob.valueOf(sigKey));
string oauth_signature = EncodingUtil.base64Encode(mac);
heads.put(EncodingUtil.urlEncode('oauth_signature','UTF-8'), EncodingUtil.urlEncode(oauth_signature,'UTF-8'));

//build the authorization header
paramHeads.clear();
paramHeads.addAll(heads.keySet());
paramHeads.sort();
string oAuth_Body = 'OAuth ';
for(String key : paramHeads){
    oAuth_Body += key+'="'+heads.get(key)+'", ';
}
oAuth_Body = oAuth_Body.subString(0, (oAuth_Body.length() - 2));
newReq.setHeader('Authorization', oAuth_Body);
system.debug('Authroization Header == '+oAuth_Body);
newReq.setHeader('Content-Type', 'application/json');

httpRtype or paste code here
sponse httpRes = new http().send(newReq);
String response = httpRes.getBody();

system.debug(response);

#2

I’m not familiar with how the Salesforce scripting works, but I’ve been able to send a direct message using twurl via this command:

twurl -t -A 'Content-type: application/json' -X POST /1.1/direct_messages/events/new.json -d '{"event": {"type": "message_create", "message_create": {"target": {"recipient_id": "nnnnn"}, "message_data": {"text": "Hello"}}}}'

It may be instructive for you to try something similar, and to trace out the headers and request/response using both twurl and whatever debugging options are available in Salesforce. I’m not aware of any change that should be required for the OAuth piece between the old and new Direct Message endpoints, this is exactly the same as it has been for a long time using standard 3-legged authorization.

One important thing to confirm is that the Content-type: application/json header is being set and sent correctly; I know that without this, twurl will fail (and all versions of twurl prior to 0.9.3 will not work as they silently ignore the instruction to set that header).


#3

Thank you andypiper,
We’ll do that as I agree interrogating that successful call will be helpful. The challenge (I believe) is the switch from the documented form-encoded body to the JSON encoded version.

Do we use the entire JSON body as the signature base string? Or just the content that we’re tweeting? Or??

I think the successful message will answer that question.

-cheers!


#4

For anyone that comes across this in the future. The answer to my question is that you omit the JSON body from the signature. We got this working shortly after figuring that out.