3

I'm trying to access Quickbooks API using the PHP SDK but getting the following error:

Refresh OAuth 2 Access token with Refresh Token failed. Body: [{"error":"invalid_grant"}].

My Tokens seem to work for 24 hours but after that I receive the error above. Each time I call the API, I am saving my updated tokens to my database:

//Client ID & Secret
$qbClientId = $this->scopeConfig->getValue('quickbooks/api/qb_client_id', $storeScope);
$qbClientSecret = $this->scopeConfig->getValue('quickbooks/api/qb_client_secret', $storeScope);

//Retrieve currently saved Refresh_Token from DB
$qbRefreshToken = $this->scopeConfig->getValue('quickbooks/api/qb_refresh_token', $storeScope);

$OAuth2LoginHelper = new OAuth2LoginHelper($qbClientId, $qbClientSecret);
$accessTokenObj = $OAuth2LoginHelper->refreshAccessTokenWithRefreshToken($qbRefreshToken);

$error = $OAuth2LoginHelper->getLastError();

if($error) {
  throw new \Exception($error);
} else {
  // The refresh token and access token expiration
  $refreshTokenValue = $accessTokenObj->getRefreshToken();
  $refreshTokenExpiry = $accessTokenObj->getRefreshTokenExpiresAt();

  // Save new Refresh Token & Expiry to DB
  $this->configInterface->saveConfig('quickbooks/api/qb_refresh_token', $this->encryptor->encrypt($refreshTokenValue), 'default', 0);
  $this->configInterface->saveConfig('quickbooks/api/qb_refresh_token_expiry', $refreshTokenExpiry, 'default', 0);

  // The access token and access token expiration
  $accessTokenValue = $accessTokenObj->getAccessToken();
  $accessTokenExpiry = $accessTokenObj->getAccessTokenExpiresAt();

  // Save new Access Token & Expiry to DB
  $this->configInterface->saveConfig('quickbooks/api/qb_access_token', $this->encryptor->encrypt($accessTokenValue), 'default', 0);
  $this->configInterface->saveConfig('quickbooks/api/qb_access_token_expiry', $accessTokenExpiry, 'default', 0);

  return DataService::Configure(array(
    'auth_mode' => 'oauth2',
    'ClientID' => $qbClientId,
    'ClientSecret' => $qbClientSecret,
    'accessTokenKey' => $accessTokenValue,
    'refreshTokenKey' => $refreshTokenValue,
    'QBORealmID' => 'MyRealmID',
    'baseUrl' => 'Development'
  ));
}

So as you can see, on each API call, I'm using the refreshAccessTokenWithRefreshToken($qbRefreshToken) method to get new Refresh and Access Tokens and saving those to my DB for next use, however I still receive invalid_grant errors after 24hours.

Any ideas?

AJK
  • 391
  • 9
  • 27

3 Answers3

1

https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/faq

Why does my refresh token expire after 24 hours?

Stale refresh tokens expire after 24 hours. Each time you refresh the access_token a new refresh_token is returned with a lifetime of 100 days. The previous refresh_token is now stale and expires after 24 hours. When refreshing the access_token, always use the latest refresh_token returned to you.

Are you sure that the latest refresh token is used?

Community
  • 1
  • 1
Jan Garaj
  • 25,598
  • 3
  • 38
  • 59
  • Yeah each time my code runs, I'm calling this function to refresh my tokens and saving the new ones to my db each time – AJK Jan 05 '20 at 01:27
  • @AJ47 Maybe someone else uses that saved refresh token as well, so your saved one became stale one. – Jan Garaj Jan 05 '20 at 06:42
  • Thanks, but in theory the code above should work continuously without ever needing to manually refresh tokens? Assuming the app is used within 100 days so the refreshToken remains valid – AJK Jan 05 '20 at 15:59
  • @AJ47 in theory, this is clear explanation of your problem. I don't have your code, infrastructure so I don't who/what is doing with saved tokens – Jan Garaj Jan 11 '20 at 08:10
1

So as you can see, on each API call, I'm using the refreshAccessTokenWithRefreshToken($qbRefreshToken) method to get new Refresh and Access Tokens and saving those to my DB for next use...

Why are you requesting a new access token and refresh token every time you make a request? Why are not checking if the old access token is stil valid?

As you stated above, you are even storing the expiration time of the access token. So you should know if it is still valid or not.

So when you are making the API request and you access token has expired, you will get a error message. In return, you can now request a new one.

davidev
  • 7,694
  • 5
  • 21
  • 56
1

I struggled with the same problem. In my case, I had updated the tokens in the database, but not everywhere in memory (specifically in the $dataService object). So continuing to use the $dataService object to make API calls was using the old tokens. D'oh!

Although I think it would still work, you do not need to refresh the tokens with every API call (as David pointed out). My solution was to make the API call and if it fails, then I refresh the tokens and make the API call again. Here is a simplified version of my code:

$user        = ...; // get from database
$dataService = getDataServiceObject();

$response = $dataService->Add(...); // hit the QBO API
$error    = $dataService->getLastError();

if ($error) {
  refreshTokens();
  $response = $dataService->Add(...); // try the API call again
}

// ... "Add" complete, onto the next thing

////////////////////////////////

function getDataServiceObject() {
  global $user;

  return DataService::Configure(array(
    'auth_mode'       => 'oauth2',
    'ClientID'        => '...',
    'ClientSecret'    => '...',
    'accessTokenKey'  => $user->getQbAccessToken(),
    'refreshTokenKey' => $user->getQbRefreshToken(),
    'QBORealmID'      => $user->getQbRealmId(),
    'baseUrl'         => '...',
  ));
}

function refreshTokens() {
  global $dataService;
  global $user;

  $OAuth2LoginHelper = $dataService->getOAuth2LoginHelper();
  $obj               = $OAuth2LoginHelper->refreshAccessTokenWithRefreshToken($user->getQbRefreshToken());
  $newAccessToken    = $obj->getAccessToken();
  $newRefreshToken   = $obj->getRefreshToken();

  // update $user and store in database
  $user->setQbAccessToken($newAccessToken);
  $user->setQbRefreshToken($newRefreshToken);
  $user->save();

  // update $dataService object
  $dataService = getDataServiceObject(); 
}
Alan P.
  • 2,898
  • 6
  • 28
  • 52