Tout commença sur un coup de tête. La lutte fut longue et dure. Et je pense que j’ai eu toutes les erreurs possibles et inimaginables quand j’ai décidé de me pencher sur l’API et l’authentification OAuth de Twitter.
J’ai voulu tout refaire from scratch, en PHP et avec curl pour faire les requêtes.
Alors, commençons par le commencement. Et le commencement, c’est sur la page qui décrit le process.
Pour faire simple, voici les étapes pour chopper votre passe partout pour user de l’api twitter sur le dos d’un utilisateur béta :
⇀ Un user se pointe sur votre site. Jusque là, rien de grandiose, mais il faut réclamer à twitter un request_token à usage unique.
⇀ Quand on a ce request_token, on redirige l’utilisateur sur twitter avec ce token. L’utilisateur se retrouve sur une page type « voulez vous autoriser l’application xxx ? »
⇀ Si il n’est pas trop bête pour cliquer sur « Allow », il se retrouve redirigé avec un oauth_token et un oauth_verifier qui vous serviront à récupérer le précieux sésame, l’oauth_token_secret.
Ca parait super simple. Sauf qu’il ne faut pas se mélanger les pinceaux.
On va avoir à faire à créer et utiliser des URI encodées. Un coup de lecture de la RFC (http://www.ietf.org/rfc/rfc3986.txt) et on pond la fonction qui va nous servir à encoder nos données:
{
return str_replace('+',' ',str_replace('%7E', '~', rawurlencode($input)));
}
Et on sort aussi une fonction qui va nous sortir à faire les requêtes:
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURL_HTTP_VERSION_1_1, TRUE);
$http_headers = array();
if($method == 'POST')
{
curl_setopt($ch, CURLOPT_POST, TRUE);
$http_headers[] = 'Expect:';
}
if(NULL !== $post_params)
{
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_params);
$http_headers[] = 'Content-Type: application/x-www-form-urlencoded';
}
if(NULL !== $headers && is_array($headers))
{
$http_headers = array_merge($http_headers, $headers);
}
if(count($http_headers))
{
curl_setopt($ch, CURLOPT_HTTPHEADER, $http_headers);
}
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
unset($ch);
return array('code' => $code, 'response' => $response);
}
On notera bien qu’on pourra faire un POST sans $post_params.
Incorrect Signature
Revenons à Twitter. Déjà, la doc nous dit qu’il faut signer toutes les requêtes. Il nous donne le pseudo code:
httpMethod + « & » +
url_encode( base_uri ) + « & » +
sorted_query_params.each { | k, v |
url_encode ( k ) + « %3D » +
url_encode ( v )
}.join(« %26″)
On le convertit assez aisement en PHP:
{
$str = $method . '&';
$str.= $this->_urlencode_rfc3986($url) . '&';
ksort($params);
$i = 0;
$param_str = '';
foreach($params as $key => $value)
{
$param_str .= $this->_urlencode_rfc3986($key) . '=' . $this->_urlencode_rfc3986($value);
if(++$i != count($params))
$param_str .= '&';
}
$str .= $this->_urlencode_rfc3986( $param_str );
$signed = base64_encode(hash_hmac("SHA1", $str, $secret, true));
return $signed;
}
Et finalement, on aura besoin pour les requêtes de construire un header Authorization: OAuth… Voici la fonction:
{
$args = array();
foreach($params as $key => $value)
{
$args[] = $this->_urlencode_rfc3986( $key ). '="' . $this->_urlencode_rfc3986($value) . '"';
}
$str = implode($args, ', ');
return(array('Authorization: OAuth ' . $str . "\n"));
}
Bon, ben là, on a un peu tout les outils nécessaires pour faire nos requêtes d’authentification puis utiliser l’API.
Oauth_verifier missing
On sort le grand jeu, avec les fonctions pour récupérer un request token puis l’access token secret:
{
$tokens = explode('&', $str);
$token_arr = array();
foreach($tokens as $token)
{
list($field, $value) = explode('=', $token);
$token_arr[ $field ] = $value;
}
return $token_arr;
}
public function getRequestToken($request_token_url, $callback_url = NULL)
{
$method = 'GET';
$nonce = sha1('nonce' + time());
$params = array(
'oauth_consumer_key' => $this->consumer_key,
'oauth_nonce' => $nonce,
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
);
if(NULL !== $callback_url)
{
$params['oauth_callback'] = $callback_url;
}
$signature = $this->buildSignature($this->consumer_secret . '&', $method, $request_token_url, $params);
$params['oauth_signature'] = $signature;
$rep_arr = $this->request($request_token_url, $method, NULL, $this->makeAuthorization($params));
$rep = $rep_arr['response'];
return $this->parseTokens( $rep );
}
public function getAccessToken($access_token_url, $oauth_token, $oauth_verifier)
{
$method = 'GET';
$nonce = sha1('nonce' + time());
$params = array(
'oauth_consumer_key' => $this->consumer_key,
'oauth_nonce' => $nonce,
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_token' => $oauth_token,
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
'oauth_verifier' => $oauth_verifier
);
$signature = $this->buildSignature($this->consumer_secret . '&', $method, $access_token_url, $params);
$params['oauth_signature'] = $signature;
$rep_arr = $this->request($access_token_url, $method, NULL, $this->makeAuthorization($params));
$rep = $rep_arr['response'];
return $this->parseTokens( $rep );
}
Failed to validate oauth signature and token twitter
Structurons le tout dans une classe dédiée (d’où les utilisations des $this), et créons un index.php pour l’utilisation:
$cs_key = '...';
$cs_secret = '...';
$request_token_url = 'https://api.twitter.com/oauth/request_token';
$access_token_url = 'https://api.twitter.com/oauth/access_token';
$authorize_url = 'https://api.twitter.com/oauth/authorize';
require_once('OAuth.class.php');
$oa = new OAuth($cs_key, $cs_secret);
// En utilisant une url de callback 'oob', vous obtiendrez un PIN pour compléter.
// Le PIN sera à considérer comme la valeur de l'oauth_verifier que l'on aurait obtenu
// si l'on avait utilisé une url de callback à la place.
$request_token = $oa->getRequestToken($request_token_url, 'oob');
echo "You must go on " . $authorize_url . '?oauth_token=' . $request_token['oauth_token'] . "\n";
echo "Please give PIN number: ";
$fp = fopen('php://stdin', 'r');
$pin = trim(fgets($fp));
$access_token = $oa->getAccessToken($access_token_url, $request_token['oauth_token'], $pin);
// On gardera précieusement le contenu de l'access_token.
Status is a duplicate
Ne reste plus qu’à l’utiliser. On crée une classe Twitter dédiée à l’API Twitter et une fonction pour mettre à jour son status:
require_once 'OAuth.class.php';
class Twitter
{
private $consumer_key = NULL;
private $consumer_secret = NULL;
private $oauth_token = NULL;
private $oauth_token_secret = NULL;
public function Twitter($consumer_key, $consumer_secret, $oauth_token, $oauth_token_secret)
{
$this->consumer_key = $consumer_key;
$this->consumer_secret = $consumer_secret;
$this->oauth_token = $oauth_token;
$this->oauth_token_secret = $oauth_token_secret;
}
public function update($message)
{
$url = 'http://api.twitter.com/1/statuses/update.json';
$method = 'POST';
$oa = new OAuth($this->consumer_key, $this->consumer_secret);
$nonce = sha1('nonce' + time());
$params = array(
'oauth_consumer_key' => $this->consumer_key,
'oauth_nonce' => $nonce,
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0',
'oauth_token' => $this->oauth_token,
);
$twitter_params = array('status' => $message);
$params = array_merge($params, $twitter_params);
$signature = $oa->buildSignature($this->consumer_secret . '&' . $this->oauth_token_secret, $method, $url, $params);
$params['oauth_signature'] = $signature;
$ret = $oa->request( $url . '?' . http_build_query($twitter_params, '', '&'), 'POST', NULL, $oa->makeAuthorization($params) );
return $ret;
}
};
On notera deux choses: L’utilisation de l’authentification OAuth pour chaque requête sur Twitter, et les paramètres qui sont passés dans l’URL et non dans le body du POST.
Et un script pour l’utiliser:
$cs_key = '...';
$cs_secret = '...';
$oauth_token = 'id-...';
$oauth_token_secret = '...';
require_once 'Twitter.class.php';
$twitter = new Twitter($cs_key, $cs_secret, $oauth_token, $oauth_token_secret);
$twitter->update('Hello world');
En espérant que ça soit utile …
Cadeau: j’ai mis le code sur github ! Vous pouvez dès maintenant consulter le source directement à l’adresse http://github.com/mycroft/YeyAnotherTwitterPhpLib.