I'm trying to obtain a request token from Twitter API in order to log in user with his twitter account and I keep getting a 401 response with "Failed to validate oauth signature and token". I'm doing this over PHP. I looked for similar questions but apparently I'm the only one crazy enough to do it from scratch without a library.
In their API documentation they talk about "percent encode" the values sent in the authorization header, I'm doing so with the urlencode() function, not sure if it's right.
To calculate the signature I use hash_hmac( 'SHA1', $signParameters, $hashKey), also not sure if it is the right one to use.
This is the request that gets generated, through cURL:
POST /oauth/request_token HTTP/1.1
Host: api.twitter.com
Accept: */*
Authorization: OAuth oauth_callback="http%3A%2F%2Fwww.soytumascota.com%2Ftwitter%2Fuser.php", oauth_consumer_key="MY_APP_KEY", oauth_nonce="0dde25902bde5f3b280f58ea642047cf", oauth_signature_method="HMAC_SHA1", oauth_timestamp="1334697987", oauth_version="1.0", oauth_signature="8313277875f20cd8a8631966a2ba273a5d13aeda"
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
Expect: 100-continue
I would really appreciate any help you can give, thank you.
EDIT: Here's the code i've written so far.
<?php
DEFINE( 'CONSUMER_KEY', 'MY_APP_KEY' );
DEFINE( 'CONSUMER_SECRET', 'MY_APP_SECRET' );
$url = 'https://api.twitter.com/oauth/request_token';
//setting OAuth parameters
$Oauth = Array();
$Oauth['oauth_callback'] = 'http://www.soytumascota.com/twitter/user.php';
$Oauth['oauth_consumer_key'] = CONSUMER_KEY;
$Oauth['oauth_nonce'] = md5( $Oauth['oauth_callback'] . CONSUMER_KEY . time() );
$Oauth['oauth_signature_method'] = 'HMAC_SHA1';
$Oauth['oauth_timestamp'] = (string) time();
$Oauth['oauth_version'] = '1.0';
//signature and authorization header are calculated inside functions
$Oauth['oauth_signature'] = calculateSignature( 'POST', $url, $Oauth );
$authorization = getAuthorizationHeader( $Oauth );
ksort( $Oauth );
//setting and sending request using cURL
$curl_session = curl_init( $url );
curl_setopt( $curl_session, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $curl_session, CURLOPT_POST, true );
curl_setopt( $curl_session, CURLINFO_HEADER_OUT, true );
curl_setopt( $curl_session, CURLOPT_HTTPHEADER, Array( 'Authorization: ' . $authorization ) );
$result = curl_exec( $curl_session );
function getAuthorizationHeader( $parameters )
{
$authorization = 'OAuth ';
$j = count( $parameters );
foreach( $parameters as $key => $val )
{
$authorization .= $key . '="' . urlencode( $val ) . '"';
if( $j-- > 1 )
{
$authorization .= ', ';
}
}
return $authorization;
}
function calculateSignature( $method, $url, $parameters, $accessToken = '' )
{
foreach( $parameters as $key => $val )
{
$foo = urlencode( $key );
unset( $parameters[$key] );
$parameters[$foo] = urlencode( $val );
}
ksort( $parameters );
$signBase = '';
$j = count( $parameters );
foreach( $parameters as $key => $val )
{
$signBase .= "{$key}={$val}";
if( $j-- > 1 )
{
$signBase .= '&';
}
}
$signBase = strtoupper( $method ) . '&' . urlencode( $url ) . '&' . urlencode( $signBase );
$signKey = urlencode( CONSUMER_SECRET ) . '&' . urlencode( $accessToken );
$signature = hash_hmac( 'SHA1', $signParameters, $hashKey);
return $signature;
}
First, 'oauth_signature_method' must be 'HMAC-SHA1' (or nothing will work).
About "percent encode", they say in their API documentation https://dev.twitter.com/docs/auth/percent-encoding-parameters they require strings to be encoded according to RFC 3986 so you should use rawurlencode instead of urlencode.
To calculate the signature they say here https://dev.twitter.com/docs/auth/creating-signature that you have to use HMAC-SHA1 hashing algorithm and convert the raw output to base64, something like:
$signature = base64_encode(hash_hmac( 'SHA1', $signBase, $signKey, true));
And finally you may need to add
//no content
curl_setopt( $curl_session, CURLOPT_POSTFIELDS, '');
//no verify certs
curl_setopt( $curl_session, CURLOPT_SSL_VERIFYPEER, FALSE);
to the curl options to get the request working.
Related
I found a sample github that is able to properly generate a signature for the API in question, their code is as follows:
so i found their github which has a sample in object oriented like this:
if ($api === 'private') {
$this->check_required_credentials();
$timestamp = $this->seconds();
$xPhemexRequestExpiry = $this->safe_integer($this->options, 'x-phemex-request-expiry', 60);
$expiry = $this->sum($timestamp, $xPhemexRequestExpiry);
$expiryString = (string) $expiry;
$headers = array(
'x-phemex-access-token' => $this->apiKey,
'x-phemex-request-expiry' => $expiryString,
);
$payload = '';
if ($method === 'POST') {
$payload = $this->json($params);
$body = $payload;
$headers['Content-Type'] = 'application/json';
}
$auth = $requestPath . $queryString . $expiryString . $payload;
$headers['x-phemex-request-signature'] = $this->hmac($this->encode($auth), $this->encode($this->secret));
}
$url = $this->urls['api'][$api] . $url;
return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
}
my code is core and looks like this, and I can not seem to get the proper result of a valid signature, any guidance possible?
$epoch=strtotime(date('r', time()).'+2 days');
$data = '{"clOrdId":"123456","ordType":"LIMIT","symbol":"BTC-USD","side":"BUY","orderQty":"0.1","price":"100"}';
$content=("/orders".$epoch.$data);
//SECRET KEY
$str = 'cn87t3Z8wLw-OR626cuAZFIUhHT2z3XSfEt6X8OBSyFjU3Ny05ZWYyLTQ3ZjItODkwNy1mODlhNWMxM2UzMWY';
$decode= base64_decode($str);
$signature = hash_hmac('SHA256',$content,'$decode');
In short I need to send the signature inside the headers like so:
"x-phemex-request-signature: $signature)",
UPDATED CODE:
$epoch=strtotime(date('r', time()).'+5 minutes');
$data = '{"clOrdId":"123456","ordType":"LIMIT","symbol":"BTC-USD","side":"BUY","orderQty":"0.1","price":"100"}';
$content="/orders".$epoch.$data;
//SECRET KEY
$str = 'cn87t3Z8wLw-OR626cuAZFIUhHT2z3XSfEt6X8OBSyFkMjE0NjU3Ny05ZWYyLTQ3ZjItODkwNy1mODlhNWMxM2UzMWY';
$signature = hash_hmac('SHA256',$auth,'cn87t3Z8wLw-OR626cuAZFIUhHT2z3XSfEt6X8OBSy***5ZWYyLTQ3ZjItODkwNy1mODlhNWMxM2UzMWY');
$headers = array(
"accept: application/json",
//API KEY
"x-phemex-access-token:e92028ca-4704-4317-8149-c1c7a7f85226",
"Content-Type: application/json",
"x-phemex-request-expiry: $epoch",
//this right here is what we need to have working proper
"x-phemex-request-signature: $signature)",
);
The format of a proper signature looks like:
API REST Request URL: https://api.phemex.com/orders
Request Path: /orders
Request Query:
Request Body: {"symbol":"BTCUSD","clOrdID":"uuid-1573058952273","side":"Sell","priceEp":93185000,"orderQty":7,"ordType":"Limit","reduceOnly":false,"timeInForce":"GoodTillCancel","takeProfitEp":0,"stopLossEp":0}
Request Expiry: 1575735514
Signature: HMacSha256( /orders + 1575735514 + {"symbol":"BTCUSD","clOrdID":"uuid-1573058952273","side":"Sell","priceEp":93185000,"orderQty":7,"ordType":"Limit","reduceOnly":false,"timeInForce":"GoodTillCancel","takeProfitEp":0,"stopLossEp":0})
signed string is /orders1575735514{"symbol":"BTCUSD","clOrdID":"uuid-1573058952273","side":"Sell","priceEp":93185000,"orderQty":7,"ordType":"Limit","reduceOnly":false,"timeInForce":"GoodTillCancel","takeProfitEp":0,"stopLossEp":0}
After getting all the credentials I'm now requesting a resource from Etsy API. It's working fine without query parameters to the url but when I add them I get signature invalid.
Here's what I got working without the query parameters in the url:
// enconding, sorting and creating the base URI
function buildBaseString($baseURI, $method, $params){
$r = array();
ksort($params);
foreach($params as $key=>$value){
$r[] = "$key=" . rawurlencode($value);
}
return $method."&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $r));
}
// create encoded header
function buildAuthorizationHeader($oauth){
$r = 'Authorization: OAuth ';
$values = array();
foreach($oauth as $key=>$value)
$values[] = "$key=\"" . rawurlencode($value) . "\"";
$r .= implode(', ', $values);
return $r;
}
$consumer_key = 'the_key';
$consumer_secret = 'the_secret';
$shop_id = '00000000000';
$oauth_access_token = 'access_token';
$oauth_access_token_secret = 'token_secret';
$receipts_by_status_url = 'https://openapi.etsy.com/v2/shops/'.$shop_id.'/receipts/opens';
$oauth = array(
'oauth_callback' => 'oob',
'oauth_consumer_key' => $consumer_key,
'oauth_nonce' => time(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_token' => $oauth_access_token,
'oauth_version' => '1.0',
);
$base_info = buildBaseString($receipts_by_status_url, 'GET', $oauth);
$composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret);
$oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true));
$oauth['oauth_signature'] = $oauth_signature;
$header = array(buildAuthorizationHeader($oauth), 'Expect:', 'Content-Type: application/x-www-form-urlencoded');
/*
* Set curl options
*/
//ob_start();
//$curl_log = fopen('php://output', 'w');
$ch = curl_init($receipts_by_status_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 25);
curl_setopt($ch, CURLOPT_VERBOSE, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// Execute curl
$result_token = curl_exec($ch);
when I add the parameters to the url:
$receipts_by_status_url = 'https://openapi.etsy.com/v2/shops/'.$shop_id.'/receipts/opens/?includes=Listings';
I get signature invalid.
$base info:
'GET&https%3A%2F%2Fopenapi.etsy.com%2Fv2%2Fshops%2F12962774%2Freceipts%2Fopen%3Fincludes%3DListings&oauth_callback%3Doob%26oauth_consumer_key%thekey%26oauth_nonce%3D1607965396%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1607965396%26oauth_token%oauthtoken%26oauth_version%3D1.0'
$oauth signature:
0Dr9wz24LU6NPkO7eKvP//HCOWk=
$header:
0 => string 'Authorization: OAuth oauth_callback="oob", oauth_consumer_key="consumerkey", oauth_nonce="1607965396", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1607965396", oauth_token="oauthtoken", oauth_version="1.0", oauth_signature="0Dr9wz24LU6NPkO7eKvP%2F%2FHCOWk%3D"' (length=301)
1 => string 'Expect:' (length=7)
2 => string 'Content-Type: application/x-www-form-urlencoded
What am I doing wrong?
I got it working.
I Manually created the base string without the function in the question, this was the root of the problem, because when I added parameters to the endpoint it didn't work.
The server takes the parameters in the authrization header and creates an OAuth signature and compares it to yours, if the don't match you'll get invalid signature like in my case.
The base string should be 3 chunks of data concatenated together and rawurlencoded:
Http method '&' url endpoint '&' params
Things to make sure:
Use a url endpoint without query parameters
Make sure the whole base string has only two ampersands.
Sort params alphabetically.
Use Postman and succeed in sending the request there, after that you can compare the signature generated in the header there to yours, when you can match it then it should work.
Make sure you're using the long url (with query params) in cURL, the clean one is only to create the base string.
I didn't notice but Etsy is actually sending the base string they created from the params in the response when the signature is invalid, all I had to do is compare mine to theirs and fix it.
Hope I helped anyone
I want to communicate with amazon lightsail with PHP curl instead of SDK because I did not wont to use composer in my project and I want to use a small number of functions at all in SDK so I try to do it via curl
When I execute the code blow I get this response
<?php
// set our constants
define('SERVICE' , 'lightsail');
define('ACCESS_KEY' , 'xxxxxxxxxxxxxxxxxxxx');
define('SECRET_KEY' , 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
define('REGION' , 'eu-west-2');
define('PROFILE' , 'default');
define('VERSION' , '20161128');
define('COMMAND' , 'getInstances');
class AWS{
static $utc_tz;
function lightsail( $payload ){
self::$utc_tz = new \DateTimeZone( 'UTC' );
$APIVERSION = VERSION;
$datestamp = new \DateTime( "now", self::$utc_tz );
$longdate = $datestamp->format( "Ymd\\THis\\Z");
$shortdate = $datestamp->format( "Ymd" );
// establish the signing key
$ksecret = 'AWS4' . SECRET_KEY;
$kdate = hash_hmac( 'sha256', $shortdate, $ksecret, true );
$kregion = hash_hmac( 'sha256', REGION, $kdate, true );
$kservice = hash_hmac( 'sha256', SERVICE, $kregion, true );
$ksigning = hash_hmac( 'sha256', 'aws4_request', $kservice, true );
// command parameters
$params = array(
'host' => SERVICE.'.'.REGION.'.amazonaws.com',
'content-type' => 'application/x-amz-json-1.1',
'x-amz-date' => $longdate,
'x-amz-target' => SERVICE.'_'. $APIVERSION . '.'.COMMAND,
'content-length' => strlen( $payload ),
'user-agent' => 'LDP',
'connection' => 'keep-alive',
);
$canonical_request = $this->createCanonicalRequest( $params, $payload );
$signed_request = hash( 'sha256', $canonical_request );
$sign_string = "AWS4-HMAC-SHA256\n{$longdate}\n$shortdate/".REGION."/".SERVICE."/aws4_request\n" . $signed_request;
$signature = hash_hmac( 'sha256', $sign_string, $ksigning );
$params['authorization'] = "AWS4-HMAC-SHA256 Credential=" . ACCESS_KEY . "/$shortdate/".REGION."/".SERVICE."/aws4_request, " .
"SignedHeaders=" . implode( ";", array_keys( $params ) ) . ", " .
"Signature=$signature";
/*
* Execute Crafted Request
*/
$url = "https://".SERVICE.".".REGION.".amazonaws.com";
$ch = curl_init();
$curl_headers = array();
foreach( $params as $p => $k )
$curl_headers[] = $p . ": " . $k;
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_TCP_NODELAY, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false );
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false );
// debug opts
curl_setopt($ch, CURLOPT_VERBOSE, true);
$verbose = fopen('php://temp', 'rw+');
curl_setopt($ch, CURLOPT_STDERR, $verbose);
$result = curl_exec($ch); // raw result
rewind($verbose);
$verboseLog = stream_get_contents($verbose);
echo "Verbose information:\n<pre>", htmlspecialchars($verboseLog), "</pre>\n";
var_dump( $result );
}
private function createCanonicalRequest( $params, $payload ){
$canonical_request = array();
$canonical_request[] = 'POST';
$canonical_request[] = '/';
$canonical_request[] = '';
$can_headers = array(
'host' => SERVICE.'.'.REGION.'.amazonaws.com'
);
foreach( $params as $k => $v )
$can_headers[ strtolower( $k ) ] = trim( $v );
uksort( $can_headers, 'strcmp' );
foreach ( $can_headers as $k => $v )
$canonical_request[] = $k . ':' . $v;
$canonical_request[] = '';
$canonical_request[] = implode( ';', array_keys( $can_headers ) );
$canonical_request[] = hash( 'sha256', $payload );
$canonical_request = implode( "\n", $canonical_request );
return $canonical_request;
}
}
$a = new AWS();
$a->lightsail( '{
"instanceName": "vodo.website"
}');
Verbose information:
* Rebuilt URL to: https://lightsail.eu-west-2.amazonaws.com/
* Trying 52.94.56.147...
* TCP_NODELAY set
* Connected to lightsail.eu-west-2.amazonaws.com (52.94.56.147) port 443 (#0)
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:#STRENGTH
* successfully set certificate verify locations:
* CAfile: c:\cert\cacert.pem
CApath: none
* NPN, negotiated HTTP1.1
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* Server certificate:
* subject: CN=lightsail.eu-west-2.amazonaws.com
* start date: 2019-10-04 00:00:00 GMT
* expire date: 2020-09-25 12:00:00 GMT
* issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
* SSL certificate verify ok.
> POST / HTTP/1.1
host: lightsail.eu-west-2.amazonaws.com
Accept: */*
content-type: application/x-amz-json-1.1
x-amz-date: 20191023T063052Z
x-amz-target: lightsail_20161128.getInstances
content-length: 38
user-agent: LDP
connection: keep-alive
authorization: AWS4-HMAC-SHA256 Credential=xxxxxxxxxxxxxxxxxxxx/20191023/eu-west-2/lightsail/aws4_request, SignedHeaders=host;content-type;x-amz-date;x-amz-target;content-length;user-agent;connection, Signature=389a5fc1aaa37bbb046faac409c52fc337585c7b8223e8abac6952e4060f989b
* upload completely sent off: 38 out of 38 bytes
< HTTP/1.1 400 Bad Request
< Server: Server
< Date: Wed, 23 Oct 2019 06:30:56 GMT
< Content-Type: application/x-amz-json-1.1
< Content-Length: 38
< Connection: keep-alive
< x-amzn-RequestId: 2cf0ef21-bee3-4430-ac98-a6986a5cafcb
<
* Connection #0 to host lightsail.eu-west-2.amazonaws.com left intact
string(38) "{"__type":"UnknownOperationException"}"
I expect the output of all my instances instead of UnknownOperationException
I have a function to create cache data xml file on my server.
I've used file_get_contents and file_put_contents, but now my hosting provider is restricting usage of them. The single way to use same function is to convert it to CURL. Can anyone give some ideas?
function checkXmlCache($xmlQuery) {
$fibulabasexml="http://any url that give xml data/"; $cachedir="/tmp/"; $ageInSeconds = 36000;
$xmlQuery=str_replace(array("'",'"'),"",$xmlQuery);
$xmlQuery2=$xmlQuery;
$long=array("stars=","TURKEY","bestprice=","country=", "location=","hotelcode=","prices=yes","tara=","simple=yes","rand()","sort=","limit=","price ","hotelname"," desc","desc");
$short=array("ST-","TR","B-","C-", "L-", "H-", "PY-", "T-", "-S-", "-R-", "S-", "LT-", "PP-","HN","-D","-D");
$xmlQuery2=str_replace($long,$short,$xmlQuery2);
$xmlQuery2=str_replace(array("xmlhotels.php","xmllocations.php"),array("XH-","XL-"),$xmlQuery2);
$xmlQuery2=str_replace(array("&","?"),array(""),$xmlQuery2);
$xmlQuery2.="_.XML";
$xmlQuery2=strip_tags($xmlQuery2);
if(!file_exists($cachedir.$xmlQuery2) || (filemtime($cachedir.$xmlQuery2) + $ageInSeconds < (time() )) ) {
$contents = file_get_contents($fibulabasexml.str_replace(" ","%20",$xmlQuery));
if(strlen($contents)>200 ) { file_put_contents($cachedir.$xmlQuery2, $contents); }
}
return($cachedir.$xmlQuery2);
Thanks for helping!
Create a simple curl function that will return the response from the request and use it in place of file_get_contents. This is not tested but it should give you the general idea perhaps.
function checkXmlCache( $xml ) {
$url="http://any url that give xml data/";
$dir="/tmp/";
$age = 36000;
$xml=str_replace( array("'",'"'), "", $xml );
$query=$xml;
$long=array("stars=","TURKEY","bestprice=","country=", "location=","hotelcode=","prices=yes","tara=","simple=yes","rand()","sort=","limit=","price ","hotelname"," desc","desc");
$short=array("ST-","TR","B-","C-","L-","H-","PY-","T-","-S-","-R-","S-","LT-","PP-","HN","-D","-D");
$query=str_replace($long,$short,$query);
$query=str_replace(array("xmlhotels.php","xmllocations.php"),array("XH-","XL-"),$query);
$query=str_replace(array("&","?"),array(""),$query);
$query.="_.XML";
$query=strip_tags( $query );
if( !file_exists( $dir.$query ) || ( filemtime( $dir.$query ) + $age < ( time() ) ) ) {
$targeturl=$url . str_replace( " ", "%20", $xml );
/* call curl function rather than file_get_contents */
$contents = curl( $targeturl );
if( strlen( $contents ) > 200 ) { file_put_contents( $dir . $query, $contents ); }
}
return( $dir.$query );
}
function curl( $url ){
$ch=curl_init( $url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER,true );
curl_setopt( $ch, CURLOPT_USERAGENT,$_SERVER['HTTP_USER_AGENT'] );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_AUTOREFERER, true );
$res=curl_exec( $ch );
curl_close( $ch );
return $res;
}
I have spent the past couple of hours trying all types of variations but according to the Twitter API this should have worked from step 1!
1 addition I have made to the script below is that I have added in:
$header = array("Expect:");
This I found helped in another question on stackoverflow from getting a denied issue / 100-continue.
Issue:
Failed to validate oauth signature and token is the response EVERY time!!!
Example of my post data:
Array ( [oauth_callback] => http://www.mysite.com//index.php [oauth_consumer_key] => hidden [oauth_nonce] => hidden [oauth_signature_method] => HMAC-SHA1 [oauth_timestamp] => 1301270847 [oauth_version] => 1.0 )
And my header data:
Array ( [0] => Expect: )
Script:
$consumer_key = "hidden";
$consumer_secret = "hidden";
function Post_Data($url,$data,$header){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS,$data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
$data['oauth_callback'] = "http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'];
$data['oauth_consumer_key'] = $consumer_key;
$data['oauth_nonce'] = md5(time());
$data['oauth_signature_method'] = "HMAC-SHA1";
$data['oauth_timestamp'] = time();
$data['oauth_version'] = "1.0";
$header = array("Expect:");
$content = Post_Data("http://api.twitter.com/oauth/request_token",$data,$header);
print_r($content);
Can anybody see an obvious mistake that I may be making here? Preferably I would not like to go with somebody elses code as most examples have full classes & massive functions, I am looking for the most simple approach!
Your problem is that you did not include the OAuth signature in your request.
You can read about the concept on this page.
A working implementation can be found here.
I faced same issue, what I was missing is passing header in to the curl request.
As shown in this question, I was also sending the $header = array('Expect:'), which was the problem in my case. I started sending signature in header with other data as below and it solved the case for me.
$header = calculateHeader($parameters, 'https://api.twitter.com/oauth/request_token');
function calculateHeader(array $parameters, $url)
{
// redefine
$url = (string) $url;
// divide into parts
$parts = parse_url($url);
// init var
$chunks = array();
// process queries
foreach($parameters as $key => $value) $chunks[] = str_replace('%25', '%', urlencode_rfc3986($key) . '="' . urlencode_rfc3986($value) . '"');
// build return
$return = 'Authorization: OAuth realm="' . $parts['scheme'] . '://' . $parts['host'] . $parts['path'] . '", ';
$return .= implode(',', $chunks);
// prepend name and OAuth part
return $return;
}
function urlencode_rfc3986($value)
{
if(is_array($value)) return array_map('urlencode_rfc3986', $value);
else
{
$search = array('+', ' ', '%7E', '%');
$replace = array('%20', '%20', '~', '%25');
return str_replace($search, $replace, urlencode($value));
}
}