Search code examples
node.jsoauthnetsuiteoauth-1.0anetsuite-rest-api

Netsuite rest API for upsert is responding with 401 response code intermittently


I have code setup to make a call to Netsuite Rest API - (upsert using external id) in Node JS using token based authentication. I can say that the code is working because the API returns 204 response code and netsuite record gets created through it. However, intermittently the API is returning 401 response code as well. Almost half no. of times the API call returned 401 code.

Infact when tried the same payload in Postman (with which 401 response was returned from the above method), it worked and 204 was returned. I have no clue why it is working intermittently when run programmatically.

Also, not sure it matters but just for info, I am running the code in Cloud Functions (GCP).

Code:

function upsertOperation(externalId, body) {

  const baseUrl = `https://${NETSUITE_ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1/${RESOURCE}`;

  const oauthNonce = crypto.randomBytes(32).toString('hex');
  const oauthTimestamp = Math.floor(Date.now() / 1000);
  const oauthSignatureMethod = 'HMAC-SHA256';
  const oauthVersion = '1.0';
  const realm = `${REALM}` // Note: Replaced space with underscore and lower case letters with upper case letters in netsuite account id. Not provided exatct value here.

  const oauthParameters = {
    oauth_consumer_key: CONSUMER_KEY,
    oauth_token: ACCESS_TOKEN,
    oauth_nonce: oauthNonce,
    oauth_timestamp: oauthTimestamp,
    oauth_signature_method: oauthSignatureMethod,
    oauth_version: '1.0'
  };

  const sortedParameters = Object.keys(oauthParameters)
    .sort()
    .map((key) => `${key}=${oauthParameters[key]}`)
    .join('&');

  const signatureBaseString = `PUT&${encodeURIComponent(baseUrl+'/eid:' + externalId)}&${encodeURIComponent(sortedParameters)}`;
  const signingKey = `${CONSUMER_SECRET}&${ACCESS_TOKEN_SECRET}`;
  const hmac = crypto.createHmac('sha256', signingKey);
  hmac.update(signatureBaseString);
  const oauthSignature = hmac.digest('base64');

  const headers = {
    'Prefer': 'transient',
    'Content-Type': 'application/json',
    'Authorization': `OAuth realm="${realm}",oauth_signature="${oauthSignature}",oauth_nonce="${oauthNonce}",oauth_signature_method="${oauthSignatureMethod}",oauth_consumer_key="${CONSUMER_KEY}",oauth_token="${ACCESS_TOKEN}",oauth_timestamp="${oauthTimestamp}",oauth_version="${oauthVersion}"`
  };

  fetch(baseUrl+'/eid:' + externalId, {
    method: 'PUT',
    headers: headers,
    body: JSON.stringify(body),
    redirect: 'follow'
  })
    .then((response) => response.json())
    .then((data) => {
      console.log('data: ' + JSON.stringify(data));
    })
    .catch((error) => {
      console.error('Error upsertOperation:', error);
    });
}

Solution

  • Edit: The signature is generated correctly and base64 encoded as it should be. However, the base64 character set includes +, /, and = which are not URL safe. Therefor the signature must be URL encoded also - encodeURIComponent(oauthSignature) - before adding it to the Authorization header.


    Here's a theory, for what it's worth...

    From https://oauth.net/core/1.0/#nonce

    Unless otherwise specified by the Service Provider, the timestamp is expressed in the number of seconds since January 1, 1970 00:00:00 GMT. The timestamp value MUST be a positive integer and MUST be equal or greater than the timestamp used in previous requests.

    IF you are making the request from two systems with different times, eg: GCP Cloud Functions and your own computer through Postman, then you could have a scenario where some requests are made with an earlier timestamp than a previous request, which will be rejected according to Oauth specification.

    If there are no requests coming from a different system, it could be that the system you are using has a different time from NetSuite's server. I have seen this be a problem previously (example), but not sure if that could cause an intermittent issue. I guess it could if the timestamp to system time comparison included rounding, and the difference between the system times is close to the rounding cut-off point.

    One other possibility is that GCP is running two instances of this practically simultaneously, or within a very short time. One instance starts, another starts with a later timestamp, but makes the http call first. In that case the first triggered request could be rejected because of its earlier timestamp.