infusionsoft / infusionsoft-php

PHP client library for the Infusionsoft API.
https://developer.infusionsoft.com/
Other
129 stars 126 forks source link

getting invalid refresh token with stored access and refresh token. #99

Closed sanatani9 closed 7 years ago

sanatani9 commented 7 years ago

this is my code,

 $infusionsoft = new Infusionsoft\Infusionsoft(array(
'clientId'     => CLIENT_ID_INFUSION,
'clientSecret' => CLIENT_SECRET_INFUSION,
'redirectUri'  => REDIRECT_URL_INFUSION,
));
    $infusionrow = $wpdb->get_row('SELECT * FROM wp_infusiontoken');
$accessToken = $infusionrow->access_token;
$refreshToken = $infusionrow->refresh_token;
$lifetime = $infusionrow->lifetime;

$old_token = new \Infusionsoft\Token();
$old_token->setAccessToken($accessToken);
$old_token->setRefreshToken($refreshToken);

// add old token to infusionsoft object
$infusionsoft->setToken($old_token);
    $tagId = 104;
try {
    if(empty($myrows))
        $infusionsoftId =$infusionsoft->contacts()->add($infusionData);
    $infusionsoft->contacts()->addToGroup($infusionsoftId, $tagId);
     } catch(\Infusionsoft\TokenExpiredException $e) {

    $infusionsoft->refreshAccessToken();
    $infstToken = $infusionsoft->getToken();
      /*here i am again storing tokens to DB */
    $data =array('access_token'=>$infstToken->getAccessToken(),'refresh_token'=>$infstToken->getRefreshToken(),'lifetime'=>$infstToken->getEndOfLife());
    $where = array('id'=>1);
    $wpdb->update('wp_infusiontoken', $data, $where);
    if(empty($myrows))
        $infusionsoftId = $infusionsoft->contacts()->add($infusionData);
    $infusionsoft->contacts()->addToGroup($infusionsoftId, $tagId);
}

`

for some time my code work fine, and then suddenly i am getting error like, Fatal error: Uncaught exception 'Infusionsoft\Http\HttpException' with message 'exception 'GuzzleHttp\Exception\ClientException' with message 'Client error:POST https://api.infusionsoft.com/tokenresulted in a400 Bad Requestresponse: {"error":"invalid_grant","error_description":"Invalid refresh token"} ' in E:\www\nginx\html\unifinedmainsite\vendor\guzzlehttp\guzzle\src\Exception\RequestException.php:107

so why is this happening ? any suggestions? i see this issues #16 #61 but nothing help me

keyboSlice commented 7 years ago

Did you manage to fix this issue? I am also having the same problem, fresh access token throws TokenExpiredException and refreshaccesstoken gives me identical error to yours.

craigthornton commented 7 years ago

@infusionsoftbamboo also experiencing this problem please rectify immediately

bgpartridge commented 7 years ago

We will have the API team look into this shortly and report and update back here.

mfairch commented 7 years ago

@archish1989 When it fails, have you tried retrying the refresh token call with the same info? Also I suggest you store a serialized version of the token in the database instead of breaking it apart so it maintains the entire token object which should prevent any weird saving issues of the refresh token.

SkywarpEvans commented 7 years ago

Any word here? My Client is FREAKING OUT!

bgpartridge commented 7 years ago

We have been unable to duplicate this issue thus far. One thought is that if multiple threads are being used they could be inadvertently invalidating the refresh token. Can you confirm that there is only one thread being used for refreshing?

feenx commented 7 years ago

I'm having this exact issue. I can see the Infusionsoft SDK is attempting this API call:

->request('POST', 'https://api.infusionsoft.com/token', array('body' => 'grant_type=refresh_token&refresh_token={refreshToken}', 'headers' => array('Authorization' => 'Basic {code}', 'Content-Type' => 'application/x-www-form-urlencoded')))

And I guaranty that {refreshToken} is exactly the same string I just got 10 minutes ago via oAuth process. Yet Infusionsoft's API responds:

{"error":"invalid_grant","error_description":"Invalid refresh token"}

Is there something else I could be missing?

bgpartridge commented 7 years ago

@pfeiferchristopher If you retry refreshing the token (using the exact same data) do you get the same error repeatedly?

feenx commented 7 years ago

@bgpartridge yes, I can re-oauth and get new tokens but I can not successfully refresh what I'm given.

bgpartridge commented 7 years ago

We have reached out to our 3rd party vendor who provides the auth token service with this thread to have them look into it as well. I will update this thread as we have new information.

bgpartridge commented 7 years ago

After a few conversations with Mashery (who provides the OAuth token endpoint for our API) here is the update:

Mashery thoroughly checked their monitoring system for this endpoint and there are no errors with the system being reported (they confirmed that their monitors check the exact use case we describe in our API documentation). They also spot checked a few heavy usage non Infusionsoft clients and found nothing reported correlating to this issue. We're left to conclude that on Mashery's end the token endpoint is functioning properly.

We confirmed with Mashery that there are two cases in which a refresh token becomes invalid:

  1. TTL for the refresh token expires
  2. When a new access token is issues, a new refresh token is provided along with it. The original refresh token is then invalid.

Mashery reiterated something mentioned earlier in this thread. If you are using multiple threads and one of them refreshes the token, the other threads will then get an invalid token error unless you have explicitly solved for this case.

One suggestion we have is to maintain a log/text file/csv etc. Update it as soon as the refresh token is used with the data time stamp. This should catch if tokens are being used multiple times.

We can duplicate the invalid token using the cases listed above because they as designed. Neither Infusionsoft nor Mashery has been able to reproduce this issue otherwise. If you verify that everything in mentioned is being accounted for, and you still see the error please provide as much detail as you can through a support ticket and we will continue to investigate.

maheshmilinda commented 7 years ago

Hi I am having the same issue. May i know the TTL for refresh token. I have one endOfLife value in my token. i suspect that endOfLife value is for access token. Can you please let me know what is TTL for refresh token. below is the exception i am getting

Infusionsoft\Http\HttpException: exception 'GuzzleHttp\Exception\ClientException' with message 'Client error: POST https://api.infusionsoft.com/token resulted in a 400 Bad Request response: {"error":"invalid_grant","error_description":"Invalid refresh token"} ' in D:\WorkDir\WebSite\vendor\guzzlehttp\guzzle\src\Exception\RequestException.php:111 Stack trace: #0 D:\WorkDir\WebSite\vendor\guzzlehttp\guzzle\src\Middleware.php(65): GuzzleHttp\Exception\RequestException::create(Object(GuzzleHttp\Psr7\Request), Object(GuzzleHttp\Psr7\Response)) #1 D:\WorkDir\WebSite\vendor\guzzlehttp\promises\src\Promise.php(203): GuzzleHttp\Middleware::GuzzleHttp{closure}

Thanks.

bgpartridge commented 7 years ago

I'm really sorry that you are still having troubles with this. We definitely want to get you over this hurdle. There is no known issue with refreshing tokens on either the Infusionsoft end or with Mashery (our API Proxy provider). This appears to be an issue in implementation and so the two best options at this point are to:

  1. File a support ticket and one of our API representatives can help you through the problem. If you post your ticket number back to me here I'll contact support personally and give them the background on the situation and get it escalated.
  2. Post in our community forum (community.infusionsoft.com) providing as much code details as you comfortably can (a reproducible code snippet would be ideal). Others there may have run across the same situation and have readily available answers to get your integration working.

For your info, auth tokens are available for 24 hours, our refresh tokens have no expiration (infinite TTL). They are however invalidated when a new auth token is issued, so each time you get a new auth token you need to make sure you're storing the newly issues refresh token as well to use that the next time you want to refresh.

harshitnayak45 commented 7 years ago

hi @bgpartridge I also have the same issue. Can you suggest me on this see below. My idea is to authorize the app only one time and get refreshed token every time.

harshitnayak45 commented 7 years ago

hi @micfai , @bgpartridge Can you provide the code for refresh access token with db connectivity.

ismailiiui commented 7 years ago

The problem is, when you authorize the app very first time and save the token in session and after that you refresh the accesstoken. The server grants you a new refreshtoken but you are not saving the most recent token in DB or session. Everytime you refresh the token, you must also pull the token and update your database with new refreshed token.

What I have done is, I created two files, one to deserealize the token and create object for my application to use and another script which runs every hour and refreshes the token and saves the token after it is refreshed.

saadizam commented 7 years ago

I keep getting the error Invalid Client..!!!

limabibi commented 7 years ago

Post an example of how to save these tokens, because I'm with the same error as above, so it's easier to understand. Ma an example of saving in DB not just the session.

Thanks.

ismailiiui commented 7 years ago

Here is my example, this is the cron script which runs midnight every night to refresh the token and saves it in DB

    global $infusionsoft;

    //get a clientid and secret from infusionsoft developer website, not main website
    $infusionsoft = new \Infusionsoft\Infusionsoft(array(
    'clientId'     => 'XXXXXXXXXXXXXXXXXXXXXXXX',
    'clientSecret' => 'XXXXXXXX',
    'redirectUri'  => 'https://www.example.com/setupinfusion.php',
    ));

    $tokenfromdb = pulltoken();
        //when you serealize the infusionsoft object it strips the backslash at the time of saving it to DB so 
        //you must put back the backslash after you retrieve the serealized object
    $tokenfromdb = str_replace('InfusionsoftToken','Infusionsoft\Token',$tokenfromdb);
        $infusionsoft->setToken(unserialize($tokenfromdb));
         if (isset($_GET['code']) and !$infusionsoft->getToken()) {
    $infusionsoft->requestAccessToken($_GET['code']);
    }
        if (!$infusionsoft->getToken()) {
    echo '<a href="' . $infusionsoft->getAuthorizationUrl() . '">Click here to authorize</a>';
    } 
        $infusionsoft->refreshAccessToken();
    //$infusionsoft->contacts()->add($contactData);
    if ($infusionsoft->getToken()) {
    // Save the serialized token to the current session or DB for subsequent requests
    $toknvar = serialize($infusionsoft->getToken());
         savetoken($toknvar);

    }

And this is the script which I include in every script where I need infusionsoft object

    global $infusionsoft;

    //get a clientid and secret from infusionsoft developer website, not main website
    $infusionsoft = new \Infusionsoft\Infusionsoft(array(
    'clientId'     => 'XXXXXXXXXXXXXXXXXXXXXXXX',
    'clientSecret' => 'XXXXXXXX',
    'redirectUri'  => 'https://www.example.com/setupinfusion.php',
    ));
    //pulltoken is the function which retrieves serialized string in DB
    $tokenfromdb = pulltoken();
        //when you serealize the infusionsoft object it strips the backslash at the time of saving it to DB so 
        //you must put back the backslash after you retrieve the serealized object
    $tokenfromdb = str_replace('InfusionsoftToken','Infusionsoft\Token',$tokenfromdb);
        $infusionsoft->setToken(unserialize($tokenfromdb));
saadizam commented 7 years ago

Thank YOu Soo much for your kind support but my work already have been done

On Tue, Jun 13, 2017 at 8:23 AM, Muhammad Ismail notifications@github.com wrote:

Here is my example, this is the cron script which runs midnight every night to refresh the token and saves it in DB

global $infusionsoft;

//get a clientid and secret from infusionsoft developer website, not main website $infusionsoft = new \Infusionsoft\Infusionsoft(array( 'clientId' => 'XXXXXXXXXXXXXXXXXXXXXXXX', 'clientSecret' => 'XXXXXXXX', 'redirectUri' => 'https://www.example.com/setupinfusion.php', ));

$tokenfromdb = pulltoken(); //when you serealize the infusionsoft object it strips the backslash at the time of saving it to DB so //you must put back the backslash after you retrieve the serealized object $tokenfromdb = str_replace('InfusionsoftToken','Infusionsoft\Token',$tokenfromdb); $infusionsoft->setToken(unserialize($tokenfromdb)); if (isset($_GET['code']) and !$infusionsoft->getToken()) { $infusionsoft->requestAccessToken($_GET['code']); } if (!$infusionsoft->getToken()) { echo 'Click here to authorize'; } $infusionsoft->refreshAccessToken(); //$infusionsoft->contacts()->add($contactData); if ($infusionsoft->getToken()) { // Save the serialized token to the current session or DB for subsequent requests $toknvar = serialize($infusionsoft->getToken()); savetoken($toknvar);

}

And this is the script which I include in every script where I need infusionsoft object

global $infusionsoft;

//get a clientid and secret from infusionsoft developer website, not main website $infusionsoft = new \Infusionsoft\Infusionsoft(array( 'clientId' => 'XXXXXXXXXXXXXXXXXXXXXXXX', 'clientSecret' => 'XXXXXXXX', 'redirectUri' => 'https://www.example.com/setupinfusion.php', )); //pulltoken is the function which retrieves serialized string in DB $tokenfromdb = pulltoken(); //when you serealize the infusionsoft object it strips the backslash at the time of saving it to DB so //you must put back the backslash after you retrieve the serealized object $tokenfromdb = str_replace('InfusionsoftToken','Infusionsoft\Token',$tokenfromdb); $infusionsoft->setToken(unserialize($tokenfromdb));

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/infusionsoft/infusionsoft-php/issues/99#issuecomment-308152964, or mute the thread https://github.com/notifications/unsubscribe-auth/ARemHo8Qyoaz_EQRAqbyJr8E4tluLDdAks5sDqlngaJpZM4LijWC .

kilrizzy commented 7 years ago

After pulling my hair out all morning working through these issues I figured I'd drop in my full path here to help anyone else that might be struggling. Sorry for the unpolished code below. Also my project's in Laravel but the idea should remain the same.

So initially I ran into the invalid token issue because I assumed there was an automated method of authentication since I only wanted to connect to just my account, I was also authenticating using credentials from my Infusionsoft account and not the credentials from keys.developer.infusionsoft.

I Setup an admin only page that opens the oauth2 authorization url (Infusionsoft::getAuthorizationUrl()). When the server is setup an admin would click this button as a first time integration

On the controller for my redirect url, I just save the data that I get back from the request. It turns out saving the code and scope is not necessary.

public function store(Request $request){
        if(isset($request->code)){
            Setting::valueSet('infusionsoft.auth.code',$request->code);
            Setting::valueSet('infusionsoft.auth.scope',$request->scope);
            $token = \Infusionsoft::requestAccessToken($request->code);
            Setting::valueSet('infusionsoft.auth.accessToken',$token->accessToken);
            Setting::valueSet('infusionsoft.auth.refreshToken',$token->refreshToken);
            Setting::valueSet('infusionsoft.auth.endOfLife',$token->endOfLife);
        }else{
            dd($request->all());
        }
        return redirect('/manage/integrate/infusionsoft'); //an admin page that just lists the settings
    }

So at this point, your token should work, although when it expires in 24 hours, your administrator would need to re-click that authorization button. So I setup a command (InfusionsoftTokenRefresh) to run twice a day to refresh the token (And re-save the new info). Below is my handle method for that command:

public function handle()
    {
        $token = new \Infusionsoft\Token([
            'access_token' => Setting::valueGet('infusionsoft.auth.accessToken'),
            'refresh_token' => Setting::valueGet('infusionsoft.auth.refreshToken'),
            'expires_in' => Setting::valueGet('infusionsoft.auth.endOfLife'),
        ]);
        \Infusionsoft::setToken($token);
        \Infusionsoft::refreshAccessToken();
        $token = \Infusionsoft::getToken();
        Setting::valueSet('infusionsoft.auth.accessToken',$token->accessToken);
        Setting::valueSet('infusionsoft.auth.refreshToken',$token->refreshToken);
        Setting::valueSet('infusionsoft.auth.endOfLife',$token->endOfLife);
    }

So then finally I was able to run a query like this:

$token = new \Infusionsoft\Token([
            'access_token' => Setting::valueGet('infusionsoft.auth.accessToken'),
            'refresh_token' => Setting::valueGet('infusionsoft.auth.refreshToken'),
            'expires_in' => Setting::valueGet('infusionsoft.auth.endOfLife'),
]);
\Infusionsoft::setToken($token);
$data = \Infusionsoft::contacts()->findByEmail('tester@tester.com',['Id']);
kilrizzy commented 7 years ago

Also had this window up all morning working on this and didn't see the update from @ismailiiui haha

caoboimolon commented 7 years ago

I can not use authentication methods, so my refresh token code returns always empty. Anything that can help me. please..

pagedesigner commented 6 years ago

im getting same error with iSDK.php {"error":"invalid_grant","error_description":"Invalid refresh token"}

khurana3192 commented 6 years ago

@bgpartridge You mentioned - " If you are using multiple threads and one of them refreshes the token, the other threads will then get an invalid token error unless you have explicitly solved for this case."

We are running multiple cron jobs to sync contacts, post notes, receive webhook payloads. At any time we detect a token is expired, we catch that, generate a new token and save the newly received paramteres - access token, refresh token and expires in our database. So, that any other script can use the updated information.

Still, at times we start receiving {"error":"invalid_grant","error_description":"Invalid refresh token"} error out of nowhere.

Is there is something wrong with our implementation or it's something which now has been marked as a known issue. I can see that multiple issues are already open mentioning the same problem and it has been almost an year and the issue still plagues us devs.

pagedesigner commented 6 years ago

@khurana3192 are you using legacy iSDK.php or new PHP integration method?

khurana3192 commented 6 years ago

@pagedesigner Hey I am using the PHP SDK (not the ISDK one)

pagedesigner commented 6 years ago

ok are you using only access token here?

$infusionsoft->setToken(unserialize($tokenfromdb));

if yes use the full returning token string from db, i also got same error until i used that instead of using only access token in it

architech99 commented 6 years ago

I started working on an Oauth integration today and I've located the root cause of this.

  1. I initiate an authorization request using the Infusionsoft Authorization URL from the SDK.
  2. It redirects to my callback and I'm able to get the access and refresh token.
  3. I save the serialized version of the Infusionsoft\Token object to the database - theoretically so I can reuse it. More on this being the problem in a moment.
  4. I set a future event in my queue to go off in 20 hours.
  5. The future event retrieves the persisted token information from the database.
  6. I deserialize the token information and pass it to a Token object constructor.
  7. I set the Token on the Infusionsoft client object (Infusionsoft\Infusionsoft).
  8. I make the refreshAccessToken call, which results in {"error":"invalid_grant","error_description":"Invalid refresh token"}

The problem is actually in the Infusionsoft\Token constructor because it transforms the original JSON properties to something different. So, when you persist the serialized token to your database (as I did in step 3 above), it doesn't match the properties anymore when you pass it into the constructor for a new Token object (as I did in step 6).

You start with the properties:

You end up (when passing the deserialized data you persisted (because that's what the Infusionsoft client object returns to to you and we developers trust is going to be accurate) with:

So, the only way to regenerate the correct token information in step 6, is to deserialize your data and then pass explicitly named array properties into the constructor for Infusionsoft\Token.

The resolution should be Infusionsoft correcting their library so it doesn't transform anything or, at the very least, gives developers a way to get the original response payload. If you want me to give you line numbers, let me know.

brumiser1550 commented 6 years ago

I save the serialized version of the Infusionsoft\Token object to the database

ok, perfect

I deserialize the token information and pass it to a Token object constructor.

Why would you pass it to the token object constructor, you already have a token object? Just pass your unserialized object to the infusionsoft class.

I think the point of the Token class is if you are starting separated information and wanting to form a Token, but since you already have a token, what is the point of the extra overhead?

This is what I do

 public function getToken($appDomainName)
    {
        $data = $this->readFile();
        if (isset($data[$appDomainName])) {
            $token = unserialize($data[$appDomainName]);
        } else {
            $token = new Token();
        }
        return $token;
    }

// Try to get a token from storage
$token = $storage->getToken($APP_NAME);

// If a token is available in storage, we tell the SDK to use that token for subsequent requests.
if (!empty($token->getAccessToken())) {
    $infusionsoft->setToken($token);
}

//Try to refresh if necessary
if ($infusionsoft->getToken()->endOfLife - time() < 7200) {
    $tokenData = $infusionsoft->refreshAccessToken();
    $token = $infusionsoft->getToken();
    $storage->saveToken($APP_NAME, $token);
}
architech99 commented 6 years ago

@brumiser1550 That's similar to how I ended up doing it. My issue arose from working with an array and expecting the property names in the documentation to remain constant (according to https://developer.infusionsoft.com/docs/xml-rpc/#authentication-request-an-access-token). I would also expect the data we work with to be consistent. Documentation says you get the access_token, refresh_token, and number of seconds until the access_token expires as a JSON array, but what you actually get back is the Token object, with different property names and the actual expiration date. Those are very different from what the documentation is indicating developers will work with and it causes undue headache by not documenting the response information accurately. The Token object is supposed to make it easier on the developer, but with inconsistent documentation, it just convolutes the entire thing.

I ultimately had to modify my application a bit to handle multiple OAuth workflows (multiple integrations). That's fine, but documentation needs to be updated and it would be very helpful to be able to authenticate against the developer account (I had to sign up for free trial of Infusionsoft just so I could test the workflow - something their documentation asks you not to do). I saw some references to being able to manually acquire an access and refresh token from your developer login, but the references don't match up with the actual interface.

danielabyan commented 5 years ago

Sloved. I had a problem with multiple threads. That is, at the same time two threads tried to update the token. @bgpartridge thanks!

saadizam commented 5 years ago

Hey,

I just signed the petition "Office of the High Commissioner for Human Rights: URGENT PETITION ON THE UN KASHMIR REPORT" and wanted to see if you could help by adding your name.

Our goal is to reach 200,000 signatures and we need more support. You can read more and sign the petition here:

http://chng.it/zXQx4HxDHj

Thanks! saad

hardikdabhi commented 4 years ago

I faced same issue. Turned out when document says parameter it is NOT url params, these parameters needs to be passed as "request body form params". Passing it in request form body solved it.

gt-parbat commented 4 years ago

Hello, Go to /src/Infusionsoft/Infusionsoft.php file and edit line no 249 and add scope to "full,refresh_token" and then again authorize then it will work.

thanks, gatetouch team