Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem getting access token for QuickBooks API #233

Closed
jacksonofalltrades opened this issue Apr 25, 2016 · 7 comments
Closed

Problem getting access token for QuickBooks API #233

jacksonofalltrades opened this issue Apr 25, 2016 · 7 comments

Comments

@jacksonofalltrades
Copy link

jacksonofalltrades commented Apr 25, 2016

Hi,

I'm completely baffled as to why I keep getting a signature invalid error. Below are the logs from my attempt to get an access token. The authorization seems to work fine.

One thing that seems odd that I noticed in the logs below is that when it prepares to request the access token, a few fields are None, which maybe shouldn't be, but I'm not sure why they weren't passed from the previous legs of the OAuth flow:
nonce, realm, timestamp, resource_owner_secret

Any help would be greatly appreciated.

DEBUG 2016-04-25 18:44:30,099 requests_oauthlib.oauth1_session Parsing token from query part of url http:https://app.histowiz.com/orders/admin/quickbooks-updated?oauth_token=qyprdmWMFvLBw43PbcmXuisKFLcmx65GE0rhoFCX8huF6pmY&oauth_verifier=rixnbq7&realmId=1421453050&dataSource=QBO
   DEBUG 2016-04-25 18:44:30,100 requests_oauthlib.oauth1_session Updating internal client token attribute.
    INFO 2016-04-25 18:44:30,100 root Parsing auth response from QB: {u'oauth_verifier': u'rixnbq7', u'oauth_token': u'qyprdmWMFvLBw43PbcmXuisKFLcmx65GE0rhoFCX8huF6pmY', u'realmId': u'1421453050', u'dataSource': u'QBO'}
   DEBUG 2016-04-25 18:44:30,100 requests_oauthlib.oauth1_session Fetching token from https://oauth.intuit.com/oauth/v1/get_access_token using client <Client nonce=None, signature_method=HMAC-SHA1, realm=None, encoding=utf-8, timestamp=None, resource_owner_secret=None, decoding=utf-8, verifier=rixnbq7, signature_type=QUERY, rsa_key=None, resource_owner_key=qyprdmWMFvLBw43PbcmXuisKFLcmx65GE0rhoFCX8huF6pmY, client_secret=****, callback_uri=https://app.histowiz.com/orders/admin/quickbooks-updated, client_key=qyprdbpcVtvjQk7le3cvOfHQQ6MBJP>
   DEBUG 2016-04-25 18:44:30,101 requests_oauthlib.oauth1_auth Signing request <PreparedRequest [POST]> using client <Client nonce=None, signature_method=HMAC-SHA1, realm=None, encoding=utf-8, timestamp=None, resource_owner_secret=None, decoding=utf-8, verifier=rixnbq7, signature_type=QUERY, rsa_key=None, resource_owner_key=qyprdmWMFvLBw43PbcmXuisKFLcmx65GE0rhoFCX8huF6pmY, client_secret=****, callback_uri=https://app.histowiz.com/orders/admin/quickbooks-updated, client_key=qyprdbpcVtvjQk7le3cvOfHQQ6MBJP>
   DEBUG 2016-04-25 18:44:30,102 requests_oauthlib.oauth1_auth Including body in call to sign: False
   DEBUG 2016-04-25 18:44:30,103 requests_oauthlib.oauth1_auth Updated url: https://oauth.intuit.com/oauth/v1/get_access_token?oauth_nonce=14292045425371585741461609870&oauth_timestamp=1461609870&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=qyprdbpcVtvjQk7le3cvOfHQQ6MBJP&oauth_token=qyprdmWMFvLBw43PbcmXuisKFLcmx65GE0rhoFCX8huF6pmY&oauth_callback=https%3A%2F%2Fapp.histowiz.com%2Forders%2Fadmin%2Fquickbooks-updated&oauth_verifier=rixnbq7&oauth_signature=F%2BJTxC%2FJQM6AuIGxRxKSGvJD9Ks%3D
   DEBUG 2016-04-25 18:44:30,104 requests_oauthlib.oauth1_auth Updated headers: {'Content-Length': '0', 'Connection': 'keep-alive', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'python-requests/2.9.1'}
   DEBUG 2016-04-25 18:44:30,104 requests_oauthlib.oauth1_auth Updated body: None
    INFO 2016-04-25 18:44:30,105 requests.packages.urllib3.connectionpool Starting new HTTPS connection (1): oauth.intuit.com
   ERROR 2016-04-25 18:44:30,457 histowiz Exception on /orders/admin/quickbooks-updated [GET]
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1687, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1360, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1358, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/site-packages/flask/app.py", line 1344, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python2.7/site-packages/flask_login.py", line 758, in decorated_view
    return func(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/flask_security/decorators.py", line 170, in decorated_view
    return fn(*args, **kwargs)
  File "/srv/histowiz/histowiz/controllers/orders.py", line 225, in admin_quickbooks_updated
    ok = qbo.update_access_tokens(request.url)
  File "/srv/histowiz/histowiz/controllers/qbint.py", line 88, in update_access_tokens
    tokens = session.fetch_access_token(self.access_token_url)
  File "/usr/local/lib/python2.7/site-packages/requests_oauthlib/oauth1_session.py", line 302, in fetch_access_token
    token = self._fetch_token(url)
  File "/usr/local/lib/python2.7/site-packages/requests_oauthlib/oauth1_session.py", line 349, in _fetch_token
    raise TokenRequestDenied(error % (r.status_code, r.text), r)
TokenRequestDenied: Token request failed with code 401, response was 'oauth_problem=signature_invalid'.
@Lukasa
Copy link
Member

Lukasa commented Apr 26, 2016

Can you provide the sample code you're using, please?

@jacksonofalltrades
Copy link
Author

jacksonofalltrades commented Apr 26, 2016

Sure, I'm doing this with a flask app. We are just trying to do back-office integration between our website and our own QuickBooks account, so that we can auto-generate invoices in Quickbooks based on orders placed on our website.

So I have this class that uses requests_oauthlib:

class QBOAuthUtil(object):
    def __init__(self, callback_uri):
        self.request_token_url = 'https://oauth.intuit.com/oauth/v1/get_request_token'
        self.auth_url = 'https://appcenter.intuit.com/Connect/Begin'
        self.access_token_url = 'https://oauth.intuit.com/oauth/v1/get_access_token'
        self.callback_uri = callback_uri
        self.session = None

    def gen_auth_url(self, client_key, client_secret):
        from requests_oauthlib import OAuth1Session
        self.session = OAuth1Session(client_key, client_secret=client_secret,
                              callback_uri=self.callback_uri)
        self.session.fetch_request_token(self.request_token_url)
        authorization_url = self.session.authorization_url(self.auth_url)
        return authorization_url

    def get_access_tokens(self, auth_callback_url):
        self.session.parse_authorization_response(auth_callback_url)
        return self.session.fetch_access_token(self.access_token_url)

And then in my url / view handlers:

@app.route("/orders/admin/refresh-quickbooks")
@login_required
@roles_required("limited-admin")
def admin_refresh_quickbooks():
    qbo = QBOAuthUtil(request)
    auth_url = qbo.gen_auth_url()
    if auth_url:
        return redirect(auth_url)
    else:
        flash("There was a problem getting the auth url for quickbooks", "error")
        redirect("/")

@app.route("/orders/admin/quickbooks-updated")
@login_required
@roles_required("limited-admin")
def admin_quickbooks_updated():
    qbo = QBOAuthUtil(request)
    ok = qbo.update_access_tokens(request.url)
    if ok:
        flash("You have successfully updated your Quickbooks credentials!", "success")
    else:
        flash("There was a problem updating your Quickbooks credentials.", "error")
    return render_template("admin_qb.html")

@jacksonofalltrades
Copy link
Author

jacksonofalltrades commented Apr 26, 2016

To clarify: ideally, since we are just doing a back-office integration, we could just use a single API key forever—OAuth protocol is overkill for this since we're not trying to provide single sign-on for our users. But Quickbooks is lame and doesn't have a way to do API integration except with OAuth.

I tried using their refresh token API and it didn't work. If it was my code, it's nearly impossible to debug the refresh token API, because it has to be called within a certain range of expiration of your existing access token.

So, to work around that, I just created this admin url for re-authorizing for any admins in the system who have access to the Quickbooks online account. The idea is I will write a crontab entry to email a link to this admin url for reauthing right before the last access token is set to expire.

I have also opened a support ticket with Quickbooks, but thus far, my experience with their support and even their answers to technical questions on their forums has been pretty useless.

@Lukasa
Copy link
Member

Lukasa commented Apr 27, 2016

I'm assuming your sample code is abbreviated (mainly because it's not correct, for example you're calling gen_auth_url with no arguments in your handler even though it requires two). I'm also assuming that you're trying to issue a brand new token rather than refreshing one: if that's not the case please let me know, as it's possible that the auth problem relates to the refresh process.

Really the biggest problem I can see here is that you throw your OAuth1Session away in between each request. I wouldn't have thought that'd be a problem, but it's in principle possible I suppose. Otherwise this feels like it's something to do with the Quickbooks side of things.

Do you want to test whether this script works for you? If it does, then the problem is somewhere in your code. If it doesn't, then I think this is QuickBooks' problem.

@jacksonofalltrades
Copy link
Author

jacksonofalltrades commented Apr 27, 2016

I am not sure how I did this, but the class definition is definitely wrong. I must have gotten it from another branch or something. Here is the correct one:

class QBOAuthUtil(object):
    def __init__(self, request, host_override=None):
        self.qbcreds = QBCredentialStore()
        self.request_token_url = 'https://oauth.intuit.com/oauth/v1/get_request_token'
        self.auth_url = 'https://appcenter.intuit.com/Connect/Begin'
        self.access_token_url = 'https://oauth.intuit.com/oauth/v1/get_access_token'
        if host_override:
            url_host = host_override
            scheme = 'http'
        else:
            url_host = request.headers['Host']
            scheme = 'https'
        self.callback_uri = "%s:https://%s%s" % (scheme, url_host, QB_CALLBACK_URI)

        v = self.qbcreds.get_vault()

        self.client_key = v['consumer_key']
        self.client_secret = v['consumer_secret']

    def gen_auth_url(self):
        session = OAuth1Session(self.client_key, client_secret=self.client_secret,
                              callback_uri=self.callback_uri, signature_type=oauthlib.oauth1.SIGNATURE_TYPE_QUERY)
        session.fetch_request_token(self.request_token_url)
        authorization_url = session.authorization_url(self.auth_url)
        if authorization_url:
            return authorization_url
        else:
            return None

    def update_access_tokens(self, auth_callback_url):
        session = OAuth1Session(self.client_key, client_secret=self.client_secret,
                              callback_uri=self.callback_uri, signature_type=oauthlib.oauth1.SIGNATURE_TYPE_QUERY)
        parse_result = session.parse_authorization_response(auth_callback_url)
        logging.info("Parsing auth response from QB: %s" % parse_result)
        # https://app.histowiz.com/orders/admin/quickbooks-updated?
        # oauth_token=qyprdnez6s820Qche3l8DU1pRr70PSYe8nrWn1V2VUYJX0xS
        # & oauth_verifier=wzmfycy
        # & realmId=1421453050
        # & dataSource=QBO

        """
        Expected format:
        https://oauth.intuit.com/oauth/v1/get_access_token?
            oauth_consumer_key=mykey
            &oauth_token=myrequesttoken
            &oauth_signature_method=HMAC-SHA1
            &oauth_signature=mysignature
            &oauth_timestamp=1392125088
            &oauth_nonce=6D7F3BC534A744AB10A55FAD8596EFF5
            &oauth_version=1.0
            &oauth_verifier=returnedverifier
        """

        tokens = session.fetch_access_token(self.access_token_url)
        if tokens:
            t = tokens['oauth_token']
            s = tokens['oauth_token_secret']
            gts = int(time.time())
            self.qbcreds.update_access_token(new_at=t, new_ats=s, new_gts=gts)
            return True

        return False

As for the OAuth1Session, I don't really see a way to keep it persistent—this is a web app. How could it exist between requests?

I will give the test program a try, though, thanks!

Btw, I am able to get as far as the parsing the authorization response, but then when I try to call fetch_access_token it fails.

@jacksonofalltrades
Copy link
Author

Ok, I have confirmed that test script does work for me. So I have to assume that you're correct about the issue being with the OAuth1Session going away. I suppose I could save all of it's contents and try to reload it. I don't suppose it has a way to do that built in?

@jacksonofalltrades
Copy link
Author

Ok, I figured it out: The reason it doesn't work if you create an OAuth1Session in two separate requests is because the get request token sets a resource_owner_secret, which is no longer present if you create a new OAuth1Session from scratch. So I am saving (temporarily) this resource_owner_secret value and adding it back to the 2nd OAuth1Session I create, and the flow works now.

Thank you for your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants