ZF-10197: Zend Http Client problem with Delicious Oauth signature

Description

I implemented the yahoo Oauth login for delicious with Zend_Oauth. All is working as expected as long as there are no spaces in the description parameter. As soon as there is a space in the description a signature error is returned from yahoo.

The problem is, that Zend_Oauth_Client (or Zend_Http_Client) uses http_build_query to create the query. http_build_query replaces all spaces with + instead of %20. So the signature created by the server is not the same as created by the client.

The bug can be fixed by adding the following on line 959 (Zend_Http_Client).


$query .= http_build_query($this->paramsGet, null, '&');
$query = str_replace('+','%20',$query);

I'm not sure if this will break other requests or what could be a better way to implement it. At the moment I created a additional socket adapter the does the replacing because I'm able to inject the adapter into the client.

More about the problem here in this thread on the bottom: http://support.delicious.com/forum/comments.php/…

Comments

May need a more detailed description as to how the client is being used (i.e. request method, authorisation scheme (any option passed into Zend_Oauth). Note that Zend_Oauth has been used elsewhere with no reported problems of this specific nature. Changing the encoding may alter signatures for working services (can't be allowed to occur), so I need to see whether this is something in the way the component is used specifically for Delicious but not for other service APIs. If this is only impacting Delicious when similar use cases work elsewhere, then it will not be fixed except for specific use cases in something like Zend_Service_Delicious where it becomes necessary but limited in scope.

Additionally, Zend_Oauth extends Zend_Http_Client which is responsible for URI encoding. This would require a fix to Zend_Http_Client if an incorrect behaviour.

I think the problem is only in the signature part of OAuth implementation. I use also OAuth with Twitter and here it works without a problem.

But as far as I can see Twitter doesn't use the whole signature sing as Delicious (Yahoo) does in step 6: http://delicious.com/help/oauthapi

Here is the code how I implemented it. The problem occurs in the third step (post) only if the variable $data['title'] has a space inside. Otherwise it works without a problem.


    public function authorize(array $data) {
        
        $config = array(
            'siteUrl' => 'https://api.login.yahoo.com/oauth/v2/…',
            'callbackUrl' => '/callback',
            'consumerKey' => $key,
            'consumerSecret' => $secret,
        );
                
        $consumer = new Zend_Oauth_Consumer($config);
        $token = $consumer->getRequestToken();
        
        $session = new Zend_Session_Namespace('delicious_oauth');
        $session->token  = $token->getToken();
        $session->secret = $token->getTokenSecret();
                
        $urlParams = $token->getResponse()->getBody();
        $url = 'https://api.login.yahoo.com/oauth/v2/request_auth?' . $urlParams;

        // redirect to $url
    }
    

    public function callback() {
        
        $config = array(
            'siteUrl' => 'https://api.login.yahoo.com/oauth/v2/get_token',
            'callbackUrl' => '/callback',
            'consumerKey' => $key,
            'consumerSecret' => $secret,
        );

        $session = new Zend_Session_Namespace('delicious_oauth');
        
        // build the token request based on the original token and secret
        $request = new Zend_Oauth_Token_Request();
        $request->setToken($session->token)
            ->setTokenSecret($session->secret);
        
        unset($session->token);
        unset($session->secret);
        
        $consumer = new Zend_Oauth_Consumer($config);
        $token = $consumer->getAccessToken($_GET, $request);
        
        $data = array('oauth_token' => $token->getToken(), 'oauth_token_secret' => $token->getTokenSecret());
        // store $data
        
    }
    
    public function post(array $data) {
        $config = array(
            'siteUrl' => 'https://api.login.yahoo.com/oauth/v2/get_token',
            'callbackUrl' => '/callback',
            'consumerKey' => $key,
            'consumerSecret' => $secret,
        );
        
        $data = $this->getData();

        $token2 = new Zend_Oauth_Token_Access();
             $token2->setToken($data['oauth_token'])
                ->setTokenSecret($data['oauth_token_secret']);
           
        
             $client = $token2->getHttpClient($config);
        $client->resetParameters();
        
        $parameters = array(
            'url' => $data['url'],
            'description' => $data['title'],
            'tags' => $data['tags'],
            'extended' => $data['note'],
        );
        
        $client->setUri('http://api.del.icio.us/v2/posts/add');
        $client->setParameterGet($parameters);
        
        $client->setMethod(Zend_Http_Client::GET);
        
        $client->setAdapter(new useKit_Http_Client_Adapter_Socket());
        $response = $client->request();
        return true;
    }

Here is also the code from my client. It only overloads the standard client and adds the str_replace function.


<?php

class useKit_Http_Client_Adapter_Socket extends Zend_Http_Client_Adapter_Socket
{
    /**
     * Send request to the remote server
     *
     * @param string        $method
     * @param Zend_Uri_Http $uri
     * @param string        $http_ver
     * @param array         $headers
     * @param string        $body
     * @return string Request as string
     */
    public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '')
    {
        // Make sure we're properly connected
        if (! $this->socket) {
            require_once 'Zend/Http/Client/Adapter/Exception.php';
            throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected');
        }

        $host = $uri->getHost();
        $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
        if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) {
            require_once 'Zend/Http/Client/Adapter/Exception.php';
            throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host');
        }

        // Save request method for later
        $this->method = $method;

        // Build request headers
        $path = $uri->getPath();
        
        $query = str_replace('+', '%20', $uri->getQuery());
                
        if ($uri->getQuery()) $path .= '?' . $query;
        $request = "{$method} {$path} HTTP/{$http_ver}\r\n";
        foreach ($headers as $k => $v) {
            if (is_string($k)) $v = ucfirst($k) . ": $v";
            $request .= "$v\r\n";
        }

        if(is_resource($body)) {
            $request .= "\r\n";
        } else {
            // Add the request body
            $request .= "\r\n" . $body;
        }
        
        // Send the request
        if (! @fwrite($this->socket, $request)) {
            require_once 'Zend/Http/Client/Adapter/Exception.php';
            throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
        }
        
        if(is_resource($body)) {
            if(stream_copy_to_stream($body, $this->socket) == 0) {
                require_once 'Zend/Http/Client/Adapter/Exception.php';
                throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
            }
        }

        return $request;
    }
}

On thing I discovered during searching for the bug is, that the Yahoo also implements one function a bit different (the PHP library can be found here: http://developer.yahoo.com/social/sdk/#php )

The url encoding is done as following


static function urlencode_rfc3986($input) {
  ...
 str_replace('+', ' ',
                           str_replace('%7E', '~', rawurlencode($input)));
  ...
  }

In the Zend_Oauth_Http_Utility it is:


    public static function urlEncode($value)
    {
        $encoded = rawurlencode($value);
        $encoded = str_replace('%7E', '~', $encoded);
        return $encoded;
    }

As defined in the OAuth Protocol, every URL should be encode with rfc3986. I'm not sure, if the second implementation is also RFC 3986 http://tools.ietf.org/html/rfc3986

But like I described before, in the end it wasn't a problem of all the oauth functions, which are creating the right signature. The problem is, that inside the signature the url is encode with rfc3986 which means, it replaces spaces with %20. But then Zend_Http_Client uses http://ch2.php.net/manual/de/… to create the queries, and replaces spaces with +. So the reponse server creates a different signature.

Fixed in trunk via a patch to Zend_Http_Client to allow for RFC 3986 strict encoding

Thanks for the fix.