5

I have written a very simple application to update my twitter status on a given condition. I have used the twitter documentation to understand the requirements of creating the OAuth signature and also how to structure the Authorization header. I then send the request with cURL in PHP.

Using the OAuth Tools on the twitter dev site, I compared both my signature base string and authorization header, and both are exactly the same:

Signature Base String

POST&https%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&oauth_consumer_key%3DYNxxxxxxxxxxxWnfI6HA%26oauth_nonce%3D31077a3c7b7bee4e4c7e2b5185041c12%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1340729904%26oauth_token%3D2991771-4csoiO2fxmWgSxxxxxxxxxxDjWj2AbyxATtiuadNE%26oauth_version%3D1.0%26status%3Dblah%2520test%2520blah.

Authorization header

Authorization: OAuth oauth_consumer_key="YN4FLBxxxxxxxxxxI6HA", oauth_nonce="31077a3c7b7bee4e4c7e2b5185041c12", oauth_signature="M2cXepcxxxxxxxxxxAImeAjE%2FHc%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1340729904", oauth_token="2991771-4cxxxxxxxxxxSmRvjzMoooMDjWj2AbyxATtiuadNE", oauth_version="1.0"

Obviously I've replaced some characters with x to hide my data, but comparing the two character for character yields exactly the same result. For reference, I hard-code the timestamp and nonce that the OAuth Tool generates so that my values can be the same for checking. My access level is set to Read and write. On that same page there is a final example - the command to run with cURL on the command line. When I run this command, it works perfectly and posts to my twitter feed with no issue.

With that in mind I believe everything I've created so far is fine, and don't think there's much point me posting the code that generates the details mentioned previously. However the code that I use to make the call, using cURL, I think is the culprit, but can't tell why:

<?php
// ...
$curl = curl_init();

curl_setopt($curl, CURLOPT_URL, $baseUrl);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_HTTPHEADER, array("Authorization: $header"));
curl_setopt($curl, CURLOPT_POSTFIELDS, array('status' => $status));
$result = json_decode(curl_exec($curl));
curl_close($curl);

var_dump($result);

Note that $baseUrl, $header and $status are the same variables used in generating the signature base string and authorization header, which matched just fine.

The output of the page when run is:

object(stdClass)#1 (2) { ["error"]=> string(34) "Could not authenticate with OAuth." ["request"]=> string(23) "/1/statuses/update.json" }

I hope there are enough details here for someone to point me in the right direction!

LeonardChallis
  • 7,759
  • 6
  • 45
  • 76
  • Have you looked at https://dev.twitter.com/discussions/305 (e.g. check the url) and https://dev.twitter.com/discussions/308 (e.g. verify HTTPS options) ? – Will Jun 26 '12 at 17:31
  • Unfortunately, yes. As I said, the signature base string and authorization header are exactly the same, and as the command line curl works perfectly, it can't be either of these. – LeonardChallis Jun 26 '12 at 18:50

2 Answers2

3

After much more searching, testing with apache_request_headers() and sticking to the notion that my data was fine and it was cURL where the problem laid, I realised that cURL was setting the Content-type of the request as multipart/form-data; and adding boundary information, obviously with a longer Content-Length field too. This meant that the status wasn't getting sent correct, I presume because of a malformed multipart/form-data; request.

The solution was to send it as a string. For instance, this works:

curl_setopt($curl, CURLOPT_POSTFIELDS, 'status='. rawurlencode($status));

But I found that there's an even nicer way (especially with multiple values, when I want to use an array):

$postfields = array('status' => $status);
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($postfields));

which looks much nicer IMHO.

LeonardChallis
  • 7,759
  • 6
  • 45
  • 76
1

I think it's your nonce. From the docs: "The oauth_nonce parameter is a unique token your application should generate for each unique request" (emphasis mine).

Caveat: I'm more familiar with OAuth 2 + Java or JavaScript rather than OAuth 1 + PHP.

If that's not it (or not the only thing), you could compare your actual HTTP request (e.g. using WireShark) to the sample request they document on that page. The note there on "Building the header string" may help too.

Will
  • 6,601
  • 3
  • 31
  • 42
  • There would be no way to distinguish who generated the nonce. As I said, the authorization header and base string are *exactly the same*, so running through curl, they wouldn't know/care who generated that nonce first. The nonce was only hard-coded to compare the values. I do think comparing the HTTP request is the way to go and indeed I changed `$baseUrl` to my own page and tested with cURL command line versus PHP cURL call, but nothing obvious has come up yet. – LeonardChallis Jun 26 '12 at 20:23
  • I'm pretty sure Twitter would cache past nonces and reject repeats, thus preventing replay attacks. See http://oauth.net/core/1.0/#nonce : "The Consumer SHALL then generate a Nonce value that is unique for all requests with that timestamp. A nonce is a random string, uniquely generated for each request. The nonce allows the Service Provider to verify that a request has never been made before and helps prevent replay attacks when requests are made over a non-secure channel (such as HTTP)." – Will Jun 26 '12 at 20:34
  • 1
    I don't think you're understanding me. The only time I hardcode the nonce is to compare my authorization header and signature base string with twitter's generated ones. I did test the generated cURL request when they matched to make sure it would complete, and it did. I don't try then to use that same nonce again in my PHP script. It's **definitely not the nonce**. In my script I have a function that generates a unique nonce. Thanks for your input, though. – LeonardChallis Jun 27 '12 at 14:13
  • Got it; yeah sorry, misunderstood. Best of luck! It's too bad it's such an unhelpful error message. =( – Will Jun 27 '12 at 18:06