ej2 / python-quickbooks

A Python library for accessing the Quickbooks API.
MIT License
396 stars 193 forks source link

Getting " 400 - invalid grant" with Quickbooks online #282

Closed user8302 closed 1 year ago

user8302 commented 1 year ago

I am using https://pypi.org/project/intuit-oauth/ and https://github.com/ej2/python-quickbook every couple of days I am getting invalid grant and need to go through the online url again to get new refresh and access tokens.

What am I doing wrong?

from intuitlib.client import AuthClient

def get_auth_client(config):
    return AuthClient(
            client_id=_get_client_id(config),
            client_secret=_get_client_secret(config),
            environment="production",
            redirect_uri={my_redirect_url},
    )

def get_quickbooks_client(config):
    auth_client = get_auth_client(config)

    quickbook_client = QuickBooks(
        auth_client=auth_client,
        company_id=config.COMPANY_ID,
        refresh_token=_get_refresh_token(config)
        )

    return quickbook_client

_get_client_id returns the correct client_id.

_get_client_secret returns the correct client secret.

config.COMPANY_ID is the Quickbooks company ID.

_get_refresh_token is the refresh token that is returned during the initial oauth set when I sign in via the authorisation url.

samhelman commented 1 year ago

I had the same issue and stumbled upon this thread.

Key points from the Intuit faq: https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/faq

"Tip: We update the value of the refresh_token value every 24 hours hours, or the next time you refresh the access tokens after 24 hours. This is an additional security measure by Intuit to reduce risk of compromise."

"Each time you make an API call and refresh your app’s access token, our server also sends your app a new value for the refresh_token."

When we initialize an AuthClient without an access_token, the refresh function is called to get a new access_token. When that api call is made, a new refresh_token is also returned that may be different from the original. We need to store the new refresh_token because the original is now considered “stale”. Using it will throw the invalid grant error.

Haven't been able to test this yet since I haven't found a way to simulate the changing refresh_token value in sandbox mode - I need to wait 24 hrs. I hope this points you in the right direction!

chrisnicholls commented 1 year ago

Check out this thread too

https://help.developer.intuit.com/s/question/0D50f00006mv4MBCAY/randomly-getting-invalid-grant-error-when-trying-to-generate-access-token-from-refresh-token-in-authclient

I just implemented the proposed solution there last night, I'm not sure yet if it will work for the long term, but in a nutshell it is to check the auth_client.refresh_token after initializing and storing if it has changed

robreiss commented 1 year ago

I treat the refresh_token as a single-use token, even though it has a 100-day lifespan. When a new access_token is obtained, the refresh_token should be replaced with the one returned alongside the new access token. In a cloud server environment where multiple instances of my program might attempt to access QuickBooks simultaneously, a race condition can occur. To address this, I serialize access to the stored refresh_token using Postgres.

I store the tokens in a table row designated for each company_id and include a column that serves as a lock when refreshing the tokens. This ensures that only one process can refresh the tokens at any given time. While I have not extensively tested this solution in a production setting, it appeared to work effectively during the limited testing conducted before the project shifted away from QuickBooks.