intuit / oauth-jsclient

Intuit's NodeJS OAuth client provides a set of methods to make it easier to work with OAuth2.0 and Open ID
https://developer.intuit.com/
Apache License 2.0
120 stars 154 forks source link

AuthResponse is not JSON #91

Closed dbowmanDT closed 3 years ago

dbowmanDT commented 4 years ago

Plugin Version: 3.0.1

Calling the makeApiCall function threw this error.

Error: Processing a request Error: AuthResponse is not JSON at AuthResponse.getJson (/home/dbowman/dtr-code/beacon/node_modules/.pnpm/registry.npmjs.org/intuit-oauth/3.0.1/node_modules/intuit-oauth/src/response/AuthResponse.js:106:29) at OAuthClient.createError (/home/dbowman/dtr-code/beacon/node_modules/.pnpm/registry.npmjs.org/intuit-oauth/3.0.1/node_modules/intuit-oauth/src/OAuthClient.js:571:31)

Looks like the response returned from QBO API was not JSON content.

Please advise.

abisalehalliprasan commented 4 years ago

Could you provide the Request Payload and Response headers if possible?

dbowmanDT commented 4 years ago

Request was to the sandbox environment: /v3/company//query?query=select Name, Id from Item Where Type = 'Service' and Name like '%' startposition 0 maxresults 11

Encoded url looks like this: https://sandbox-quickbooks.api.intuit.com//v3/company/4620816365048034790/query?query=%20select%20Name%2C%20Id%20from%20Item%20Where%20Type%20%3D%20'Service'%20and%20Name%20like%20'%25'%20startposition%200%20maxresults%2011

I do not have the response headers, I should have put them in the issue when I encountered the error. I do not know how to reproduce consistently.

abisalehalliprasan commented 4 years ago

@dbowmanDT : The query that was passed to the makeApiCall() was not URI-encoded.

The query you are trying to use is :

select Name, Id from Item Where Type = 'Service' and Name like '%' startposition 0 maxresults 11

You can pass the query using makeApiCall() as shown below : ( minorversion is optional )

makeApiCall({
      url: `${url}v3/company/${companyID}/query?query=select%20Name%2C%20Id%20from%20Item%20Where%20Type%20%3D%20%27Service%27%20and%20Name%20like%20%27%25%27%20startposition%200%20maxresults%2011&minorversion=51`,
    })

For now, you can use the above-mentioned workaround.

Since this is an OAuth2.0 library, we will think about how to add this to a full-fledged SDK.

dbowmanDT commented 4 years ago

@abisalehalliprasan I am already properly encoding the URI when it was sent and got the error in question.

I provided the encoded and non-encoded query in my comment for your convenience.

akila-arasan commented 3 years ago

@abisalehalliprasan @dbowmanDT I am also facing the same issue, I digged into the source code and the problem was in the processresponse method.

  AuthResponse.prototype.processResponse = function processResponse(response) {
  this.response = response || '';
  this.body = (response && response.body) || '';
  this.json = this.body ? JSON.parse(this.body) : null; // JSON.parse is failing.
  this.intuit_tid = (response && response.headers && response.headers.intuit_tid) || '';
};

the response body value returned as follows,

 body: '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2020-08-17T02:01:11.228-07:00"><Fault type="AuthenticationFault"><Error code="100"><Message>General Authentication Error</Message><Detail>AuthenticationErrorGeneral: SRV-110-Authentication Failure , statusCode: 401</Detail></Error></Fault></IntuitResponse>'

As the body is returned in xml it is failing in process response.

Also, I make api call only if the oauthClient.isAccessTokenValid() returns true. However there is authentication error. Am I missing some thing?

the query is select * from Account where AccountType = 'Cost of Goods Sold'

Please help.

abisalehalliprasan commented 3 years ago

@akila-arasan : Tried to reproduce the error and I am not running into the same issue. I am suspecting if there is a mismatch with the Base URL used to make the API call and the Keys provided. When a token is invalid the SDK throws an error with the below format ( secure details are masked )

  authResponse: AuthResponse {
    token: Token {
      realmId: '462xxxxxxxxxxx867780',
      token_type: 'bearer',
      access_token: 'eyJlbmMixxxxxxxxxxxxxFFxk-hjxtA',
      refresh_token: 'AB11606448419RxxxxxxxxxxxxxxxxxxxxgRlWpTn0xFffVpoq',
      expires_in: 3600,
      x_refresh_token_expires_in: 8726400,
      id_token: '',
      latency: 60000,
      createdAt: 1597722019184,
      state: 'intuit-test'
    },
    response: Response {
      Url: [Url],
      rawHeaders: [Array],
      body: '{"warnings":null,"intuitObject":null,"fault":{"error":[{"message":"message=AuthenticationFailed; errorCode=003200; statusCode=401","detail":"Could not decrypt JWT.","code":"3200","element":null}],"type":"AUTHENTICATION"},"report":null,"queryResponse":null,"batchItemResponse":[],"attachableResponse":[],"syncErrorResponse":null,"requestId":null,"time":1597722025429,"status":null,"cdcresponse":[]}',
      status: 401,
      statusText: 'Unauthorized'
    },
    body: '{"warnings":null,"intuitObject":null,"fault":{"error":[{"message":"message=AuthenticationFailed; errorCode=003200; statusCode=401","detail":"Could not decrypt JWT.","code":"3200","element":null}],"type":"AUTHENTICATION"},"report":null,"queryResponse":null,"batchItemResponse":[],"attachableResponse":[],"syncErrorResponse":null,"requestId":null,"time":1597722025429,"status":null,"cdcresponse":[]}',
    json: {
      warnings: null,
      intuitObject: null,
      fault: [Object],
      report: null,
      queryResponse: null,
      batchItemResponse: [],
      attachableResponse: [],
      syncErrorResponse: null,
      requestId: null,
      time: 1597722025429,
      status: null,
      cdcresponse: []
    },
    intuit_tid: '1-5f3b4da9-087a28ed878e2215f4eb3a28'
  },
  originalMessage: 'Response has an Error',
  error: 'Unauthorized',
  error_description: 'Unauthorized',
  intuit_tid: '1-5f3b4da9-087a28ed878e2215f4eb3a28'
}

Could you share the sample code snippet where you are performing the query select * from Account where AccountType = 'Cost of Goods Sold' that would help us debug the question at hand? OR if you could provide the intuit_tid / appID and timestamp of the request I could run by the logs to see what is missing.

akila-arasan commented 3 years ago

@abisalehalliprasan Thanks for debugging.

The Base URL that I use to make the query is - https://sandbox-quickbooks.api.intuit.com/v3/company/realmId/query?query= select * from Account where AccountType = 'Cost of Goods Sold'

After establishing a connection between QB and our app, we try to export items to inventory, we need to check if the company has a income / expense account before adding items to inventory, that's why we make the above call.

the code snippet for the above api call is

const accountNameList = await oauthClient.makeApiCall({
    url: `${QBUrl}v3/company/${realmId}/query?query=${query}`,
  })
    .then((res) => {
      customSetToken(oauthClient, res.token);
      return res;
    })
    .catch(function(err) {
      return err;
    })

the realmID is - 4620816365033885370 timestamp - 1597738603040

But the question here is oauthClient.isAccessTokenValid() call returns true only after that I make the call to query account. So if there is an authentication failure shouldn't this oauthClient.isAccessTokenValid() fail?

I Logged the request, the code fails in the following method in sdk,

OAuthClient.prototype.getTokenRequest = function getTokenRequest(request) {
  console.log('request', request);
  const authResponse = new AuthResponse({ token: this.token });

  return (new Promise(((resolve) => {
    resolve(this.loadResponse(request));
  }))).then((response) => {
    authResponse.processResponse(response);

    if (!authResponse.valid()) throw new Error('Response has an Error');

    return authResponse;
  }).catch((e) => {
    if (!e.authResponse) e = this.createError(e, authResponse);
    throw e;
  });
};

The request object consoled was like this,

{ 
url: 'https://sandbox-quickbooks.api.intuit.com/v3/company/4620816365033885370/query?query=SELECT * FROM Account where AccountType = \'Cost of Goods Sold\'',
 method: 'GET',
  headers: 
   { Authorization: 'Bearer  xxxxxxxx',
     Accept: 'application/json',
     'User-Agent': 'Intuit-OAuthClient-JS_[object Object]_Darwin_19.4.0_darwin' 
    } }
abisalehalliprasan commented 3 years ago

I looked up our logs based on the realmID and timestamp you provided. It looks like you are making the call for an incorrect realm. The realm from the logs for the specific error shows ( 193514846595064 ) for the exact timestamp you provided.

And hence the oauthClient.isAccessTokenValid() is returning true as the token is still valid. ( under 1 hour duration )

I was able to reproduce the same error with the incorrect token/realm combination as shown below :

<IntuitResponse xmlns="http://schema.intuit.com/finance/v3" time="2020-08-18T21:08:26.926-07:00">
    <Fault type="AuthenticationFault">
        <Error code="100">
            <Message>General Authentication Error</Message>
            <Detail>AuthenticationErrorGeneral: SRV-110-Authentication Failure , statusCode: 401</Detail>
        </Error>
    </Fault>
</IntuitResponse>

Could you verify if your application is able to associate the token object with realmID based on the storage mechanism that you are using? This should hopefully fix the issue.

As for the above error format XML handling, I will put in a PR for the next release.

akila-arasan commented 3 years ago

@abisalehalliprasan Thanks ! The problem was in the cron that refreshes expired QB access tokens. Now the associations are correct.

abisalehalliprasan commented 3 years ago

Awesome. I will mark it as resolved. 👍

RicardoAReyes commented 3 years ago

Hi @abisalehalliprasan do you have a timetable for when you plan to submit the PR for this issue? Our team would like to test our app with this AuthReponse JSON fix and plan accordingly. Thank you.

Isaacmeedinaa commented 1 year ago

Has this been resolved?