Tag Archives: api

L’authentification Oauth 1.0a avec Twitter

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:

    public function _urlencode_rfc3986($input)
    {
         return str_replace('+',' ',str_replace('%7E', '~', rawurlencode($input)));
    }

Et on sort aussi une fonction qui va nous sortir à faire les requêtes:

    public function request($url, $method = 'GET', $post_params = NULL, $headers = NULL)
    {
        $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:

    public function buildSignature($secret, $method, $url, $params)
    {
        $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:

    public function makeAuthorization($params)
    {
        $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:

    public function parseTokens($str)
    {
        $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:

<?php

$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:

<?php
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:

<?php
$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.

Créer une application facebook offline en utilisant l’api graph et le php-sdk

Mon but: Créer une application php pouvant pooler des informations sur un compte facebook sans que la personne en question ne soit forcement connectée à ce moment là.

Outils: Mon serveur web, l’api graph facebook et son php-sdk (http://github.com/facebook/php-sdk).

Initialement, je voulais faire un tutoriel sur l’intégration et l’utilisation de ce sdk avec symfony, mais cela aurait été un poil trop long et sans réel intérèt.

Avant toute chose, il ne faut pas oublier de créer une application facebook (facebook.com/developers), de noter son id, la clef api et la clef secrète, et de récupérer le php-sdk (hint: ‘git clone http://github.com/facebook/php-sdk.git’).

Vue la nature de l’application (type « connect », oauth et utilisation du sdk), quelques modifications sont à effectuer dans les settings:
Dans Connexion, spécifier l’URL connect (qui pointera vers l’endroit où vous allez mettre votre index.php), et activez « Canvas Session Parameter » et « OAuth 2.0 for Canvas (beta) » dans l’onglet Migrations.

La démo va comporter 2 parties: Une partie web (index.php), nécessaire pour enregistrer les utilisateurs dans l’application, les authentifier et récupérer le token d’accès, et une seconde, totalement « offline » qui permettra de récupérer les données dans facebook sans intervention tierce (fetch.php).

Le workflow d’enregistrement de l’utilisateur, de l’ajout des permissions et de la création de l’access token offline est le suivant:

⇀ L’utilisateur se pointe sur notre index.php, on lui affiche un lien pour se logger sur facebook ou pour ajouter l’application, qu’il doit cliquer;
⇀ En cliquant, il se retrouve sur facebook, se loggue si besoin, autorise l’application;
⇀ Il revient sur notre page, et nous lui demandons qu’il retourne sur facebook pour qu’il nous autorise à utiliser le mode offline et certaines autres permissions;
⇀ Facebook redirige l’utilisateur vers notre script, avec un paramètre « code »;
⇀ Nous effectuons une requète secrète avec le code donné et notre clef secrete vers facebook, pour récupérer le token illimité, que l’on sauvegarde précieusement.

Ces étapes peuvent être simplifiées (à base de Location: ), mais j’ai préféré bien distinguer les différentes étapes pour que ça soit clair (pour vous comme pour moi, plus tard).

Le premier script que je propose est plus simple que celui d’exemple proposé par le SDK de facebook. Il s’appelle donc index.php, sera le script par défaut dans mon répertoire web:

<?php

require_once 'php-sdk/src/facebook.php';

// Le script a besoin de connaitre un minimum d'information sur l'application facebook.
// On donne donc son ID et ses clefs d'utilisation:
$app_id = '... application id ...';
$api_key = '... la clef API ...';
$secret_key = '... la clef secrète ...';

// Construit une interface facebook avec les infos de notre application
// Noter qu'il est obligatoire d'avoir les extensions PHP json et curl.
$facebook = new Facebook(array(
                            'appId' => $app_id,
                            'secret' => $secret_key,
                            'cookie' => true
                        ));

// Recupere la session
// Si celle ci n'est pas valide, aucune interaction avec fb ne sera possible
$session = $facebook->getSession();

if($session)
{
    try {
        // On appelle l'API graph, ici graph.facebook.com/me
        // /me contient le nom de l'utilisateur mais aussi son ID facebook.
        $me = $facebook->api('/me');
    }
    catch(FacebookApiException $e) {
    }
}

// Si aucune session n'est detectee, alors on affiche un lien de login.
if( ! $session || ! $me )
{
    $loginUrl = $facebook->getLoginUrl();
    echo '<a href=' . $loginUrl . '>login</a>';
}
else
{
    $logoutUrl = $facebook->getLogoutUrl();
    // On pourrait dumper le résultat de l'appel à /me.
    echo 'Mon nom est ' . $me['name'] . '.';
    echo '<br />';
    echo '<a href=' . $logoutUrl . '>logout</a><br />';
}

Ensuite, pour effectuer des requêtes offline, il faut demander l’autorisation de l’utilisateur. Un appel à l’api Oauth 2.0 de facebook est ici nécessaire (voir http://developers.facebook.com/docs/api#authorization).

Pour ce faire, on rajoute donc une fonction « buildOAuthPermissionLink » et son lien prêt à être utilisé:

function buildOAuthPermissionLink()
{
    global $app_id;

    $my_site = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'];

    $fb_link = 'https://graph.facebook.com/oauth/authorize?';
    $fb_link.= 'client_id=' . $app_id;
    $fb_link.= '&redirect_uri=' . $my_site;
    $fb_link.= '&scope=' . 'offline_access,read_stream';

    return $fb_link;
}
/* ... */
    echo 'Mon nom est ' . $me['name'] . '.';
    echo '<br />';
    echo 'Ask for permissions: <a href="' . buildOAuthPermissionLink() . '">click</a> ';
    echo '<br />';

Dès lors qu’on cliquera sur ce lien, on sera redirigé vers facebook qui, après avoir obtenu l’autorisation de l’utilisateur de partager les données, nous enverra un code qu’on pourra utiliser pour l’échanger contre un access_token spécifique de durée illimité. Cette fois-ci, c’est à nous de faire la requète:

if( isset($_REQUEST['code']) )
{
    $my_site = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'];

    $fb_link = 'https://graph.facebook.com/oauth/access_token?';
    $fb_link.= 'client_id=' . $app_id;
    $fb_link.= '&redirect_uri=' . $my_site;
    $fb_link.= '&client_secret=' . $secret_key;
    $fb_link.= '&code=' . $_REQUEST['code'];

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $fb_link);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HEADER, 0);

    $ans = curl_exec($ch);
    curl_close($ch);

    list($field, $value) = explode('=', $ans);
    if($field == 'access_token')
    {
        echo "Access token is '" . $value . "'<br />";
    }
}

Le token donné sera la clef de voute des appels effectués en mode offline. Il faut la conserver précieusement.
On va créer un 2nd script qui effectuera les appels en mode déconnecté. On l’appelle ‘fetch.php‘:

<?php

require_once 'php-sdk/src/facebook.php';

// De meme pour index.php, on a besoin des elements d'informations de l'applicaiton:
$app_id = '... l application id ...';
$api_key = '... la clef API ...';
$secret_key = '... la clef secrète ...';

// On cree l'objet:
$facebook = new Facebook(array(
                            'appId' => $app_id,
                            'secret' => $secret_key,
                            // Pas de cookie ... On est en mode offline, on donnera l'access token apres.
                        ));

// On fait un appel public, sans clef (ceci est l'url customisable)
$res = $facebook->api( '/patrick.marie' );
print_r($res);

// Ici le token qu'on a precieusement conservé suite à la fin de l'authentification OAuth
$access_token = '... l access token que l on a récupéré ...';

// On prepare un appel à /me. On a besoin de donner l'access token:
$params = array(
            'access_token' =>  $access_token
          );

$res = $facebook->api( '/me', 'GET', $params );
print_r($res);

// Dans l'index.php, j'ai demandé l'autorisation 'read_stream'. On va l'exploiter
// pour récupérer les derniers éléments de mon wall:
$wall = $facebook->api( '/me/feed', 'GET', $params );
print_r($wall);

// Ou ceux de mes amis:
$home = $facebook->api( '/me/home', 'GET', $params );
print_r($home);

Et voilà, cela clot cette première partie. Dans une seconde, je reviendrai sur les différents appels possibles au php-sdk, sur les POST possibles, les fonctions legacy et les requètes FQL.