Intuit Quickbooks Accounting API OAuth with Developer 根本无法工作

Intuit Quickbooks Accounting API OAuth with Developer not working at all

我只想使用 Quickbooks API 获取信息(这似乎可以通过他们的 API 实现)。我在他们的开发站点上设置了一个应用程序,将其链接到我创建的 Quickbooks 公司,并试图 运行 此代码以从 curl 响应中获取任何内容,但我得到的只是授权失败 (401) 消息。为什么不被授权?研究这个网站 12 个小时,他们提供的示例 none 甚至可以工作。我正在使用此页面作为参考:https://developer.intuit.com/docs/0050_quickbooks_api/0010_your_first_request/rest_essentials_for_the_quickbooks_api and this: https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/0015_calling_data_services#/The_authorization_header

我的index.php文件如下:

    <?php

define('IS_SANDBOX', 1);

require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'oAuth.php');

// GET baseURL/v3/company/companyID/resourceName/entityID
// consumer and consumer_secret
$oAuth = new QuickBooks_IPP_OAuth('qyprdwX21R3klmiskW3AaYLnDRGNLn', 'FDPpxScC6CIgoA07Uc2NYtZJk45CqNDI1Gw4zntn');

$request = array(
    'url' => array(
        'base_request_uri' => IS_SANDBOX == 1 ? 'https://sandbox-quickbooks.api.intuit.com' : 'https://quickbooks.api.intuit.com',
        'version' => 'v3',
        'company' => 'company',
        'companyID' => '123145768959777'
    ),
    'query' => 'SELECT * FROM ESTIMATE',
    'headers' => array(
        'Host' => IS_SANDBOX == 1 ? 'sandbox-quickbooks.api.intuit.com' : 'quickbooks.api.intuit.com',
        'Accept' => 'application/json',
        'User-Agent' => 'APIExplorer'
    )
);

$request_url = implode('/', $request['url']) . '/query?query=' . str_replace('+', ' ', str_replace('%7E', '~', rawurlencode($request['query']))) . '&minorversion=4';

// token, and token_secret
$headers = $oAuth->sign('GET', $request_url, 'qyprdaiy37CxGCuB8ow8XK76FYii3rnRU4AIQrHsZDcVFNnV', 'wWcpmPffdPABp6LNNyYgnraTft7bgdygAmTML0aB');

$request['headers']['Authorization'] = 'OAuth ' . array_pop($headers);

$response = curl($request_url, $request['headers']);

echo '<pre>', var_dump($response), '</pre>';
echo '<pre>', var_dump($request['headers']), '</pre>';


function curl($url, $headers) {
    try {
        $request_headers = array();
        $ch = curl_init();

        if (FALSE === $ch)
            throw new Exception('failed to initialize');

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        if (!empty($headers)) {

            foreach($headers as $key => $value)
            {
                if ($key == 'GET')
                {
                    $request_headers[] = $key . ' ' . $value;
                    continue;
                }

                $request_headers[] = $key . ': ' . $value;
            }

            curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL Verfication, so we can get all info from non-SSL site!
        }

        $data = curl_exec($ch);
        $header = curl_getinfo($ch);

        echo '<h2>Curl Get Info</h2>';
        echo '<pre>', var_dump($header), '</pre>';

        if (FALSE === $data)
            throw new Exception(curl_error($ch), curl_errno($ch));
        else
            return $data;

        curl_close($ch);
    } catch(Exception $e) {
        trigger_error(sprintf(
                'Curl failed with error #%d: %s',
                $e->getCode(), $e->getMessage()), E_USER_ERROR);
    }
}

echo '<pre>', var_dump($request_url), '</pre>';


?>

我的 oAuth.php 文件如下所示:

<?php

/**
 * QuickBooks PHP DevKit
 * 
 * Copyright (c) 2010 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 * 
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt 
 * 
 * @package QuickBooks
 */

class QuickBooks_IPP_OAuth
{
    private $_secrets;

    protected $_oauth_consumer_key;
    protected $_oauth_consumer_secret;

    protected $_oauth_access_token;
    protected $_oauth_access_token_secret;

    protected $_version = null;
    protected $_signature = null;
    protected $_keyfile;

    /**
     * 
     */
    const NONCE = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

    const METHOD_POST = 'POST';
    const METHOD_GET = 'GET';
    const METHOD_PUT = 'PUT';
    const METHOD_DELETE = 'DELETE';

    const DEFAULT_VERSION = '1.0';
    const DEFAULT_SIGNATURE = 'HMAC-SHA1';

    const SIGNATURE_PLAINTEXT = 'PLAINTEXT';
    const SIGNATURE_HMAC = 'HMAC-SHA1';
    const SIGNATURE_RSA = 'RSA-SHA1';

    /** 
     * Create our OAuth instance
     */
    public function __construct($oauth_consumer_key, $oauth_consumer_secret)
    {
        $this->_oauth_consumer_key = $oauth_consumer_key;
        $this->_oauth_consumer_secret = $oauth_consumer_secret;

        $this->_version = QuickBooks_IPP_OAuth::DEFAULT_VERSION;
        $this->_signature = QuickBooks_IPP_OAuth::DEFAULT_SIGNATURE;
    }

    /**
     * Set the signature method
     * 
     * 
     */
    public function signature($method, $keyfile = null)
    {
        $this->_signature = $method;
        $this->_keyfile = $keyfile;
    }

    /**
     * Sign an OAuth request and return the signing data (auth string, URL, etc.)
     *
     * 
     */
    public function sign($method, $url, $oauth_token = null, $oauth_token_secret = null, $params = array()) 
    {
        /*
        print('got in: [' . $method . '], ' . $url);
        print_r($params);
        print('<br /><br /><br />');
        */

        if (!is_array($params))
        {
            $params = array();
        }

        $params = array_merge($params, array(
            'oauth_consumer_key' => $this->_oauth_consumer_key, 
            'oauth_signature_method' => $this->_signature, 
            'oauth_nonce' => $this->_nonce(), 
            'oauth_timestamp' => $this->_timestamp(), 
            'oauth_version' => $this->_version,
            ));

        // Add in the tokens if they were passed in
        if ($oauth_token)
        {
            $params['oauth_token'] = $oauth_token;
        }

        if ($oauth_token_secret)
        {
            $params['oauth_secret'] = $oauth_token_secret;
        }

        // Generate the signature
        $signature_and_basestring = $this->_generateSignature($this->_signature, $method, $url, $params);

        $params['oauth_signature'] = $signature_and_basestring[1];

        /*
        print('<pre>');
        print('BASE STRING IS [' . $signature_and_basestring[0] . ']' . "\n\n");
        print('SIGNATURE IS: [' . $params['oauth_signature'] . ']');
        print('</pre>');
        */

        $normalized = $this->_normalize($params);

        /*
        print('NORMALIZE 1 [' . $normalized . ']' . "\n");
        print('NORMZLIZE 2 [' . $this->_normalize2($params) . ']' . "\n");
        */

        if (false !== ($pos = strpos($url, '?')))
        {
            $url = substr($url, 0, $pos);
        }

        $normalized_url = $url . '?' . $normalized;         // normalized URL

        return array (
            0 => $signature_and_basestring[0],      // signature basestring
            1 => $signature_and_basestring[1],      // signature
            2 => $normalized_url, 
            3 => $this->_generateHeader($params, $normalized),  // header string
            );
    }

    protected function _generateHeader($params, $normalized) 
    {
        // oauth_signature="' . $this->_escape($params['oauth_signature']) . '", 
        $str = '';

        if (isset($params['oauth_token']))
            $str .= rawurlencode('oauth_token') . '="' . rawurlencode($params['oauth_token']) . '", ';

        $nonce = rawurlencode(md5(mt_rand()));

        $nonce_chars = str_split($nonce);

        $formatted_nonce = '';
        foreach($nonce_chars as $n => $chr)
        {
            if (in_array($n, array(8, 12, 16, 20)))
                $formatted_nonce .= '-';

            $formatted_nonce .= $chr;
        }

        $str .= rawurlencode('oauth_nonce') . '="' . $formatted_nonce . '", ' . 
            rawurlencode('oauth_consumer_key') . '="' . rawurlencode($params['oauth_consumer_key']) . '", ' . 
            rawurlencode(oauth_signature_method) . '="' . rawurlencode($params['oauth_signature_method']) . '", ' .
            rawurlencode(oauth_timestamp) . '="' . rawurlencode($params['oauth_timestamp']) . '", ' . 
            rawurlencode(oauth_version) . '="' . rawurlencode($params['oauth_version']) . '", ' . 
            rawurlencode(oauth_signature) . '="' . $this->_escape($params['oauth_signature']) . '"';

        return str_replace(array(' ', '  ', '   '), '', str_replace(array("\r", "\n", "\t"), ' ', $str));
    }

    /**
     * 
     * 
     */
    protected function _escape($str) 
    {
        if ($str === false)
        {
            return $str;
        }
        else
        {
            return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode($str)));
        }
    }

    protected function _timestamp()
    {
        //return 1326976195;

        //return 1318622958;
        return time();
    }

    protected function _nonce($len = 5) 
    {
        //return '1234';

        $tmp = str_split(QuickBooks_IPP_OAuth::NONCE);
        shuffle($tmp);

        //return 'kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg';
        return substr(implode('', $tmp), 0, $len);
    }

    protected function _normalize($params)
    {   
        $normalized = array();

        ksort($params);
        foreach ($params as $key => $value)
        {
            // all names and values are already urlencoded, exclude the oauth signature
            if ($key != 'oauth_secret')
            {
                if (is_array($value))
                {
                    $sort = $value;
                    sort($sort);
                    foreach ($sort as $subkey => $subvalue)
                    {
                        $normalized[] = $this->_escape($key) . '=' . $this->_escape($subvalue);
                    }
                }
                else
                {
                    $normalized[] = $this->_escape($key) . '=' . $this->_escape($value);
                }
            }
        }

        return implode('&', $normalized);
    }

    protected function _generateSignature($signature, $method, $url, $params = array()) 
    {
        /*
        print('<pre>params for signing');
        print_r($params);
        print('</pre>');
        */

        //if (false !== strpos($url, 'get_access'))
        /*if (true)
        {
            print($url . '<br />' . "\r\n\r\n");
            die('NORMALIZE MINE [' . $this->_normalize($params) . ']');
        }*/

        /*
        print('<pre>');
        print('NORMALIZING [' . "\n");
        print($this->_normalize($params) . "]\n\n\n");
        print('SECRET KEY FOR SIGNING [' . $secret . ']' . "\n");
        print('</pre>');
        */

        if (false !== ($pos = strpos($url, '?')))
        {
            $tmp = array();
            parse_str(substr($url, $pos + 1), $tmp);

            // Bad hack for magic quotes... *sigh* stupid PHP
            if (get_magic_quotes_gpc())
            {
                foreach ($tmp as $key => $value)
                {
                    if (!is_array($value))
                    {
                        $tmp[$key] = stripslashes($value);
                    }
                }
            }

            $params = array_merge($tmp, $params);

            $url = substr($url, 0, $pos);
        }

        //print('url [' . $url . ']' . "\n");
        //print_r($params);

        $sbs = $this->_escape($method) . '&' . $this->_escape($url) . '&' . $this->_escape($this->_normalize($params));

        //print('sbs [' . $sbs . ']' . "\n");

        // Which signature method? 
        switch ($signature)
        {
            case QuickBooks_IPP_OAuth::SIGNATURE_HMAC:
                return $this->_generateSignature_HMAC($sbs, $method, $url, $params);    
            case QuickBooks_IPP_OAuth::SIGNATURE_RSA:
                return $this->_generateSignature_RSA($sbs, $method, $url, $params);
        }

        return false;
    }


    /*
        // Pull the private key ID from the certificate
        $privatekeyid = openssl_get_privatekey($cert);

        // Sign using the key
        $sig = false;
        $ok  = openssl_sign($base_string, $sig, $privatekeyid);   

        // Release the key resource
        openssl_free_key($privatekeyid);

        base64_encode($sig)
    */


    protected function _generateSignature_RSA($sbs, $method, $url, $params = array())
    {
        // $res = ... 
        $res = openssl_pkey_get_private('file://' . $this->_keyfile);

        /*
        print('key id is: [');
        print_r($res);
        print(']');
        print("\n\n\n");
        */

        $signature = null;
        $retr = openssl_sign($sbs, $signature, $res);

        openssl_free_key($res);

        return array(
            0 => $sbs, 
            1 => base64_encode($signature), 
            );
    }



    /*
    $key = $request->urlencode($consumer_secret).'&'.$request->urlencode($token_secret);
    $signature = base64_encode(hash_hmac("sha1", $base_string, $key, true));
    */  

    protected function _generateSignature_HMAC($sbs, $method, $url, $params = array())
    {
        $secret = $this->_escape($this->_oauth_consumer_secret);

        $secret .= '&';

        if (!empty($params['oauth_secret']))
        {
            $secret .= $this->_escape($params['oauth_secret']);
        }

        //print('generating signature from [' . $secret . ']' . "\n\n");

        return array(
            0 => $sbs, 
            1 => base64_encode(hash_hmac('sha1', $sbs, $secret, true)), 
            );
    }
}
?>

$request['headers'] 看起来像这样:

array(4) {
  ["Host"]=>
  string(33) "sandbox-quickbooks.api.intuit.com"
  ["Accept"]=>
  string(16) "application/json"
  ["User-Agent"]=>
  string(11) "APIExplorer"
  ["Authorization"]=>
  string(306) "OAuth oauth_token="qyprdaiy37CxGCuB8ow8XK76FYii3rnRU4AIQrHsZDcVFNnV",oauth_nonce="189f7f21-6dd9-c136-e208-0f33141feea5",oauth_consumer_key="qyprdwX21R3klmiskW3AaYLnDRGNLn",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1462545676",oauth_version="1.0",oauth_signature="BIpYveqCxlfVT4Ps4qJypS%2BXHh8%3D""
}

响应如下所示:

message=ApplicationAuthenticationFailed; errorCode=003200; statusCode=401
            SignatureBaseString: GET&https%3A%2F%2Fsandbox-quickbooks.api.intuit.com%2Fv3%2Fcompany%2F123145768959777%2Fquery&minorversion%3D4%26oauth_consumer_key%3DqyprdwX21R3klmiskW3AaYLnDRGNLn%26oauth_nonce%3D189f7f21-6dd9-c136-e208-0f33141feea5%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1462545676%26oauth_token%3Dqyprdaiy37CxGCuB8ow8XK76FYii3rnRU4AIQrHsZDcVFNnV%26oauth_version%3D1.0%26query%3DSELECT%2520%252A%2520FROM%2520ESTIMATE

$request_url 看起来像这样:

https://sandbox-quickbooks.api.intuit.com/v3/company/123145768959777/query?query=SELECT%20%2A%20FROM%20ESTIMATE&minorversion=4

我是不是忘了在这里做点什么?或者也许有些东西不正确?我应该从 ID 为 123145768959777 的 Quickbook 公司获取所有估计,但我得到的只是 401 授权失败消息。

Am I forgetting to do something here? Or perhaps something is not correct somehow?

是的,绝对是。具体见下文:

$headers = $oAuth->sign(null, ...

null 不是有效的 HTTP 请求方法。有效的 HTTP 请求方法是 GETPOST 等。请参考 HTTP 规范和 OAuth 规范。

$headers = $oAuth->sign(null, $_SERVER['REQUEST_URI'],

为什么要签署服务器请求 URI?您应该签署 URL, 您正在将 curl 请求发送到 ,而不是 URL 用户正在访问您自己的网站。

$headers = $oAuth->sign(null, $_SERVER['REQUEST_URI'], 'qyprdaiy37CxGCuB8ow8XK76FYii3rnRU4AIQrHsZDcVFNnV', 'wWcpmPffdPABp6LNNyYgnraTft7bgdygAmTML0aB');

您不能对 OAuth 访问令牌和密码进行硬编码。它们每 6 个月更改一次,因此必须存储在某个地方的 database/file 中,这样您就可以在不每 6 个月编辑代码的情况下更改它们。

$request_url = implode('/', $request['url']) . '/query?query=' . str_replace('+', ' ', str_replace('%7E', '~', rawurlencode($request['query']))) . '&minorversion=4';

这是您应该签署的URL。

I should be getting All Estimates from within the Quickbook Company with ID of 123145768959777, but all I'm getting is 401 Authorization Failure messages.

如果您需要进一步的帮助,post 您的实际 HTTP 请求和响应会很有意义。在没有真正看到发送的请求和收到的响应的情况下,没有多少人能告诉你。

另外...您意识到所有这些艰苦的工作都已经为您完成了,使用您从中获取代码的库,对吧?例如你不需要做任何你正在做的事情——这只是重新发明轮子。只是做:

require_once dirname(__FILE__) . '/config.php';

$EstimateService = new QuickBooks_IPP_Service_Estimate();
$estimates = $EstimateService->query($Context, $realm, "SELECT * FROM Estimate STARTPOSITION 1 MAXRESULTS 10");

foreach ($estimates as $Estimate)
{
    print('Estimate # ' . $Estimate->getDocNumber() . "\n");
}

有用的链接: