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?
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();
}
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?
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.
Related
I am trying to build a simple MS Graph API call to get familiar with Graph.
However, I can't get it to work. MS Graph keeps giving the error that my token has expired, while it's not.
Code:
<?php
require_once('C:\inetpub\site6\vendor\autoload.php');
// Using newest version of TheNetworg Oauth2
$provider = new TheNetworg\OAuth2\Client\Provider\Azure([
'clientId' => '***************',
'clientSecret' => '**********',
'redirectUri' => 'https://app2.***/test.php'
]);
// Set to use v2 API, skip the line or set the value to Azure::ENDPOINT_VERSION_1_0 if willing to use v1 API
$provider->defaultEndPointVersion = TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;
$baseGraphUri = $provider->getRootMicrosoftGraphUri(null);
//echo $baseGraphUri;
$provider->tenant = '*********.onmicrosoft.com'; //Azure AD ID
$provider->authWithResource;
$provider->scope = $baseGraphUri . '/.default';
$token = $provider->getAccessToken('client_credentials', ['scope' => $provider->scope]);
// echo $token;
// Set up our request to the API
$ref= 'users/someuser#mytenant.com';
$response = $provider->get($ref, $token, $headers = []);
// Store the result as an object
$result = json_decode( $response->getBody() );
?>
But I keep getting ended up with error:
PHP Fatal error: Uncaught
League\OAuth2\Client\Provider\Exception\IdentityProviderException:
Your access token has expired. Please renew it before submitting the
request. in
C:\inetpub\site6\vendor\thenetworg\oauth2-azure\src\Provider\Azure.php:394
What am I doing wrong? When I google the error, I get a lot of results telling that I am trying to access MS Graph with an Azure AD Graph token, but when I do echo $baseGraphUri; I really tells me graph.microsoft.com.
I found out what the error is. Although I use "$baseGraphUri = $provider->getRootMicrosoftGraphUri(null);", the library still connects to the Azure AD API instead of the Microsoft Graph API. So it authenticates with the wrong kind (aud) of token.
Adding this line fixed the problem:
$provider->urlAPI = 'https://graph.microsoft.com/';
I have a Magento 2.3 store that I'm trying to sync some data to Quickbooks Online.
I've created a QBO App but this is my first time using oauth and I'm a bit confused on how to store and use the access / refresh tokens.
According to Quickbooks doc I need to store the latest refresh token:
Each access token can only be valid for an hour after its creation. If you try to make an API call after an hour with the same access token, the request will be blocked by QBO. That is what refresh token used for. It is used to request a new access token after access token expired, so you can still access to the QBO company after an hour. Just remember, whenever you make a refreshToken API call, always STORE THE LATEST REFRESH TOKEN value in your session or database. In QuickBooks Online OAuth 2 protocol, it is not the access token you should store, it is the refresh token you need to store.
So my question is, how do I properly store and call upon my refresh token to generate a new access token each time my API makes a call to sync data.
Currently, I'm directly using my OAuth tokens by hard coding them into my helper file:
<?php
namespace Company\Module\Helper;
use QuickBooksOnline\API\DataService\DataService;
class Data extends \Magento\Framework\App\Helper\AbstractHelper
{
public function getConfigurationSetting()
{
$dataService = DataService::Configure(array(
'auth_mode' => 'oauth2',
'ClientID' => '<<my ClientID',
'ClientSecret' => '<<my ClientSecret>>',
'accessTokenKey' => 'xxxxxx',
'refreshTokenKey' => 'xxxxxx',
'QBORealmID' => "123xxxxxxx",
'baseUrl' => 'Development'
));
$OAuth2LoginHelper = $dataService->getOAuth2LoginHelper();
$refreshedAccessTokenObj = $OAuth2LoginHelper->refreshToken();
$error = $OAuth2LoginHelper->getLastError();
if ($error){
$dataService->throwExceptionOnError(true);
} else {
$dataService->updateOAuth2Token($refreshedAccessTokenObj);
}
return $dataService;
}
}
And then I'm calling that from my controller:
<?php
namespace Company\Module\Observer;
use Magento\Framework\Event\ObserverInterface;
use QuickBooksOnline\API\DataService\DataService;
class CreateQbInvoice implements ObserverInterface
{
protected $helperData;
public function __construct(
\Company\Module\Helper\Data $helperData
){
$this->helperData = $helperData;
}
public function execute()
{
// Prep Data Services
$dataService = $this->helperData->getConfigurationSetting();
...
Now this works until my access token expires and I need to generate a new one, I'm just not sure how to update my access token and store the new refresh token properly to keep access to my app always refreshed.
once you get access token. use that to get token and refresh token.
you will get token, refresh token, expiry for token, expiry for refresh token
save all data in database with current time.
for QuickBook token will expire after few hours but refresh token will not expire up to 1 year.
so for every request you will first check if token expire get new token with refresh token. refresh token will return token and new refresh token replace that will previous one
because you don't have and mechanism to refresh the token . i guess you need a permanent access token.
https://www.oauth.com/oauth2-servers/access-tokens/access-token-lifetime/
use QuickBooksOnline\API\DataService\DataService;
$dataService = DataService::Configure(array(
'auth_mode' => 'oauth2',
'ClientID' => 'your client id',
'ClientSecret' => 'your client secret',
'RedirectURI' =>'redirect url',
'scope' => "com.intuit.quickbooks.accounting openid profile",
'baseUrl' => 'development or production'
));
$OAuth2LoginHelper = $dataService->getOAuth2LoginHelper();
$authorizationCodeUrl = $OAuth2LoginHelper->getAuthorizationCodeURL();
if( isset($_GET['code']) ) {
$accessTokenObj = $OAuth2LoginHelper->exchangeAuthorizationCodeForToken( $_GET['code'], 'your company id') );
// save these for later use
$refreshTokenValue = $accessTokenObj->getRefreshToken();
// Expires every 12 hours.
$refreshTokenExpiry = $accessTokenObj->getRefreshTokenExpiresAt();
// The access token and access token expiration.
$accessTokenValue = $accessTokenObj->getAccessToken();
$accessTokenExpiry = $accessTokenObj->getAccessTokenExpiresAt();
}
I'm currently trying to implement a way to synchronize my PHP App calendar with the Outlook calendar of my clients, using Azure API.
I use OAuth2 and the custom Microsoft provider by Steven Maguire.
I currently run in an issue where I get an error in my response :
{"error":"unsupported_grant_type","error_description":"The provided value for the input parameter 'grant_type' is not valid. Expected values are the following: 'authorization_code', 'refresh_token'."}
I'm having trouble understanding why the grant_type password is not supported, even though it says on the documentation of Azure that it is.
The request looks like this :
client_id=44bef79b-**********************&client_secret=H****************&redirect_uri=https%3A%2F%2F192.168.1.123%2Fmapeyral%2Fcalendarsync.php&grant_type=password&username=******************&password=***********&scope=openid%20profile%20offline_access%20Calendars.ReadWrite
The Authorize url used is : https://login.live.com/oauth20_token.srf
as defined in the Steven Maguire provider.
The header contains the content-type application/x-www-form-urlencoded (I've seen a lot of post where this was what caused the error).
Some of my code :
$this->provider = new Microsoft([
'clientId' => MicrosoftGraphConstants::CLIENT_ID,
'clientSecret' => MicrosoftGraphConstants::CLIENT_SECRET,
'redirectUri' => MicrosoftGraphConstants::REDIRECT_URI,
'urlAuthorize' => MicrosoftGraphConstants::AUTHORITY_URL . MicrosoftGraphConstants::AUTHORIZE_ENDPOINT,
'urlAccessToken' => MicrosoftGraphConstants::AUTHORITY_URL . MicrosoftGraphConstants::TOKEN_ENDPOINT,
'urlResourceOwnerDetails' => MicrosoftGraphConstants::RESOURCE_ID,
'scope' => MicrosoftGraphConstants::SCOPES
]);
if ($_SERVER['REQUEST_METHOD'] === 'GET' && !isset($_GET['code']))
{
// Try getting access token from Database
$workingAccount = $GLOBALS['AppUI']->getState('working_account');
if (isset($workingAccount))
{
// DB access
$DB = new DatabaseConnection();
$dbAccess = $DB->getConnection();
$contactData = DBUserUtils::getContactDataFromEmail($GLOBALS['AppUI']->getState('working_account'), $dbAccess);
// If at least one user contact found
if (!is_null($contactData))
{
// If has refresh token => fill session variables using refresh token
if (!is_null($contactData['contact_refreshToken']))
{
log_msg('debug.log', 'Has refresh token');
$GLOBALS['AppUI']->setState('preferred_username', $contactData['contact_email']);
$GLOBALS['AppUI']->setState('given_name', $contactData['contact_first_name']." ".$contactData['contact_last_name']);
// Get new tokens
$newAccessToken = $this->provider->getAccessToken('refresh_token', [
'refresh_token' => $contactData['contact_refreshToken']
]);
// Update tokens and DB
$GLOBALS['AppUI']->setState('refresh_token', $newAccessToken->getRefreshToken());
$GLOBALS['AppUI']->setState('access_token', $newAccessToken->getToken());
DBOAuthUtils::updateTokenForUser($contactData['contact_id'], $GLOBALS['AppUI']->getState('refresh_token'), $dbAccess);
$this->redirectTo($redirectURL);
}
else
{
$this->getAccessToken();
}
}
else
{
$this->getAccessToken();
}
}
else
{
$this->getAccessToken();
}
function getAccessToken(){
$accessToken = $this->provider->getAccessToken('password', [
'username' => '*************',
'password' => '********',
'scope' => MicrosoftGraphConstants::SCOPES
]);
}
During the first try it doesn't pass the if (isset($workingAccount)) condition (as expected) and go straight to the last else.
Code is a bit ugly for now but I don't think it has an impact on my problem.
Any help would be appreciated !
Thanks
Edit : added code
That helped me, the problem was that I need to use Azure Active Directory and not Azure AD 2.0.
Problem solved !
I am following the docs from link below:
https://developers.google.com/+/mobile/android/sign-in#enable_server-side_api_access_for_your_app
Specifically the part that says:
If you do not require offline access, you can retrieve the access token and send it to your server over a secure connection. You can obtain the access token directly using GoogleAuthUtil.getToken() by specifying the scopes without your server's OAuth 2.0 client ID. For example:
I retrieve the access token like this:
accessToken = GoogleAuthUtil.getToken(
AuthenticatorActivity.this,
Plus.AccountApi.getAccountName(Common.mGoogleApiClient),
"oauth2:https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/plus.login email"
);
After I retrieve the access token I send it to a web server, on the web server i can see that it's a valid access token by calling
https://www.googleapis.com/oauth2/v1/tokeninfo?access_token='.$_POST['google_access_token']
The request above returns the android apps client id, it also returns the users email correctly.
The problem is that when I try to run $client->authenticate($_POST['google_access_token']); I get an exception with the message: "invalid_grant: Incorrect token type".
To prevent getToken caching I always invalidate the token in android app:
if (accessToken != null && !accessToken.isEmpty()) {
GoogleAuthUtil.invalidateToken(AuthenticatorActivity.this, accessToken);
}
Here's the php code:
if (!isset($_POST['google_access_token'])) {
throw new Exception('missing google_access_token');
}
$client = new \Google_Client();
$client->setApplicationName("GiverHub");
$client->setClientId($this->config->item('google_client_id'));
$client->setClientSecret($this->config->item('google_client_secret'));
$client->setDeveloperKey($this->config->item('google_developer_key'));
$client->setRedirectUri($this->config->item('google_redirect_uri'));
$client->setScopes([
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/plus.me',
'email',
]);
try {
$client->authenticate($_POST['google_access_token']); // if i remove this the rest of the code below works! ...
$reqUrl = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token='.$_POST['google_access_token'];
$req = new \Google_Http_Request($reqUrl);
$io = $client->getIo();
$response = $io->executeRequest($req);
$response = $response[0];
$response = json_decode($response, true);
if ($response === null) {
throw new Exception('Failed to check token. response null');
}
if ($response['issued_to'] !== '466530377541-s7cfm34jpf818gbr0547pndpq9songkg.apps.googleusercontent.com') {
throw new Exception('Invalid access token. issued to wrong client id: '. print_r($response, true));
}
if (!isset($response['user_id'])) {
throw new Exception('Missing user_id');
}
if (!isset($response['email'])) {
throw new Exception('Missing email');
}
/** #var \Entity\User $user */
$user = Common::create_member_google([
'id' => $response['user_id'],
'email' => $response['email'],
'given_name' => '',
'family_name' => '',
]);
$user->login($this->session);
if ($user instanceof \Entity\User) {
echo json_encode( [ 'success' => true, 'user' => $user ] );
} else {
echo json_encode( [ 'success' => false, 'msg' => $user ] );
}
} catch(Exception $e) {
echo json_encode(['success' => false, 'msg' => $e->getMessage()]);
}
The above code works if i remove the $client->authenticate(); line ... The problem is that I can't get the given_name / family_name etc .. only email / google_user_id from the tokeninfo ...
Any thoughts about why the key works for tokeninfo but not for authenticate?
I have tried many different variations of the scopes .. both on the server side and the android side ..
The $client->authenticate() method doesn't quite do what you're trying to do. It takes a one-time code from an earlier OAuth transaction and exchanges it for the access token. In your case - you're saying you already have the access token.
You should be able to call $client->setAccessToken() to set the token instead, so it may look something like
$client->setAccessToken($_POST['google_access_token']);
This is the solution I came up with after user158443 suggested I use $client->setAccessToken();
// first json_encode the access token before sending it to $client->setAccessToken();
$json_encoded_access_token = json_encode([
'access_token' => $_POST['google_access_token'],
'created' => time(), // make up values for these.. otherwise the client thinks the token has expired..
'expires_in' => time()+60 // made up a value in the future...
]);
// and then set it
$client->setAccessToken($json_encoded_access_token);
// and then get userinfo or whatever you want from google api !! :)
$oauth2 = new \Google_Service_Oauth2($client);
$user_info = $oauth2->userinfo->get();
NOTE: it's probably not smart to "emulate" the expires_in and created that i just did if you are in production ... You should probably call tokeninfo first and get the expires time from there...
NOTE: I still have no idea how to get a refresh token for this... but I don't need one for my use case..
I am developing gadget with tech requirements: "no Cookie, no Session".
I have the following code:
<?php
class LinkedIn
{
private $options;
private $consumer;
private $client;
private $token;
public function __construct($params)
{
// set Zend_Oauth_Consumer options
$this->options = array(
'version' => '1.0',
'localUrl' => $params['localUrl'],
'callbackUrl' => $params['callbackUrl'],
'requestTokenUrl' => 'https://api.linkedin.com/uas/oauth/requestToken',
'userAuthorizationUrl' => 'https://api.linkedin.com/uas/oauth/authorize',
'accessTokenUrl' => 'https://api.linkedin.com/uas/oauth/accessToken',
'consumerKey' => $params['apiKey'],
'consumerSecret' => $params['secretKey']
);
// instanciate Zend_Oauth_Consumer class
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Oauth_Consumer');
$this->consumer = new Zend_Oauth_Consumer($this->options);
}
public function connect()
{
// Start Session to be able to store Request Token & Access Token
session_start ();
if (!isset ($_SESSION ['ACCESS_TOKEN'])) {
// We do not have any Access token Yet
if (! empty ($_GET)) {
// SECTION_IF
// But We have some parameters passed throw the URL
// Get the LinkedIn Access Token
$this->token = $this->consumer->getAccessToken ($_GET, unserialize($_SESSION ['REQUEST_TOKEN']));
// Store the LinkedIn Access Token
$_SESSION ['ACCESS_TOKEN'] = serialize ($this->token);
} else {
// SECTION_ELSE
// We have Nothing
// Start Requesting a LinkedIn Request Token
$this->token = $this->consumer->getRequestToken ();
// Store the LinkedIn Request Token
$_SESSION ['REQUEST_TOKEN'] = serialize ($this->token);
// Redirect the Web User to LinkedIn Authentication Page
$this->consumer->redirect ();
}
} else {
// We've already Got a LinkedIn Access Token
// Restore The LinkedIn Access Token
$this->token = unserialize ($_SESSION ['ACCESS_TOKEN']);
}
// Use HTTP Client with built-in OAuth request handling
$this->client = $this->token->getHttpClient($this->options);
}
}
It's working perfect. But REQUEST_TOKEN stored in SESSION. How can I put it to query string in SECTION_ELSE, and get it back in SECTION_IF? Thanks for all the advice.
The key point is that your system needs to:
1. persist the OAuth tokens between user requests to your server, and
2. tie them to a specific user.
Using a session, whose id comes from either a cookie or from the querystring, is one way to do that.
But if sessions are off the table, then you need some other way to identify the current user and store his OAuth tokens.
If you are truly working in a no-session environment, then how do you even know who the user is? Basic Auth? In the absence of user authentication on your side, I don't see how you'll be able associate OAuth tokens to specific users.