Unable to POST with OAuth1 Authentication

  • Goal: POST data to NetSuite's REST API using OAuth1 authentication

  • Steps: I've configured the API as a resource in our account and provided all the needed variables for OAuth1 authentication to work.

  • Details: I confirmed the auth works by making a number of GET requests to the API, but when I try to do a POST request, I get a 401 Unauthorized error. I've tried the exact same request in Postman, with the same credentials and payload, and it is successful, so it isn't a permissions issue.
    I can't do much else by way of debugging, but it would seem like there might be an issue with the way Retool generates the signature base string which would change based on the method. I've seen some references to the order of the values in the auth header, but again, the fact that it works for GET but not POST makes that seem less likely to be the issue.

1 Like

Can you share some screenshot of your resource config and query? (Feel free to scrub any sensitive information from the screenshot)

Sure, though there really isn't much to it, it's all environment vars.

I tested it using webhook.site and everything looked right, but NetSuite says invalid login attempt no matter what when issuing a POST call. Particularly strange because GET works, so something in the creation of the Auth string is breaking when it's a post, and the only thing I can find that changes is the signature base string.

I have a temporary workaround now where I've created a second resource with no authentication, I call that inside a function in my workflow and have implemented my own OAuth1 function to create the Authentication string. This allowed me to test more easily against Postman as well because I could control the nonce and timestamp to eventually get them to match. I don't love this because I've had to hardcode keys and secrets in my function and it's going to be harder to maintain, so I'm hoping it just helps figure where the problem is and how to correct it.

there are a couple of things I've found. First, I've found this post from @kabirdis stating that Oauth1 currently doesn't support user-based authentication (Nov 22 - Pass xml data to rest api - #2 by Kabirdas).

The OAuth 1.0 flow we offer doesn't support user-based authentication at the moment. A couple other members of the team have been investigating this particular connection with etrade and, unfortunately, we don't have a working solution. It is something the dev team will take a look at and we can let you know here when it's included!

I remember reading somewhere that some requests are sent sandboxed from a different server resulting in different origin addresses and XSS errors? Otherwise the only other thing I could find is a stackoverflow topic where OP was getting this error in Postman, which you actually have working but there were a couple replies that I thought could be relevant (postman - Getting 401 from Netsuite REST API - Stack Overflow)

  • realm (your account id, if using a sandbox, make sure the realm looks like 1234567_SB1, with an _ and not a -)

...For me, the role I was assigned to did not have permissions to Rest Web Services in Netsuite.

Appreciate you looking into this! I came across all of those posts as well but they all differed from my situation a little bit. As you pointed out, I can get it to work in Postman, and the particularly confusing part is that I can get GET requests to work in Retool, it's just POST requests that don't. That lead me to believe it could be either a permissions issue, or an auth header issue. I ruled out permissions because the same credentials are working in Postman.

So at the moment I can only assume it's the way Retool is creating the Auth header for the POST requests since the method is part of the signature base string, which is used to generate the oauth_signature part of the header. I can't intercept/override the request to test by modifying the none or timestamp though, so I think I need someone from Retool to dig into this more if it's going to get solved.

If anyone else runs into this, my workaround has been to manually create a function to generate the header and call the API. I used a Retool resource for this with no auth, but I might swap it out for a pure fetch call because you can't set the method dynamically when calling a resource, and you need to know the method to generate the appropriate auth header.

2 Likes

I have the exact same problem. GET requests to NetSuite work from Retool, both GET and POST requests working from Postman, POST requests failing from Retool with 401 Unauthorized

Hi @Sol_Parker,

This is still an issue - I'm working with NS data again, searched the forums, and found my own post on the topic.

I have come up with a workaround, a bit of a pain, but at least it allows us to access Netsuite with Retool. It also leads me to believe this IS possible in Retool, there is just a difference in how Retool and NetSuite generate their OAuth1 Signatures.

Anyway, the "fix" for you and anyone else who runs into this:

Create a new resource for NetSuite, don't fill in anything except the base URL for the resource. Leave everything else empty/blank, even the Authentication section.

In your workflow, add the Crypto-JS library.

Create a new JS function, I call mine createNSAuth, use this code:

const CryptoJS = require('crypto-js')

function generateNonce(length = 32) {
    let result = '';
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const charactersLength = characters.length;
    for (let i = 0; i < length; i++) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
}
const oauth_timestamp =  Math.floor(new Date().getTime() / 1000); //'1701732104'// Math.floor(Date.now() / 1000);

function percentEncode(str) {
  return encodeURIComponent(str).replace(
    /[!*()']/g,
    (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`
  );
}

function generateSignature(baseString, consumerSecret, tokenSecret) {
  const key = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
  return CryptoJS.HmacSHA256(baseString, key).toString(CryptoJS.enc.Base64);
}

function buildAuthHeader() {
  const params = {
    oauth_consumer_key: retoolContext.configVars.netsuite_sb_CONSUMER_KEY,
    oauth_nonce: nonce || generateNonce(),
    oauth_signature_method: "HMAC-SHA256",
    oauth_timestamp: timestamp || oauth_timestamp,
    oauth_token: retoolContext.configVars.netsuite_sb_TOKEN_ID,
    oauth_version: "1.0",
    // include other request-specific parameters here
  };

  const httpMethod = method || "GET"; // or 'POST', 'PUT', etc.
  console.log(httpMethod)
  const baseUrl = percentEncode(
    `${retoolContext.configVars.netsuite_sb_baseurl}/${path}`
  );

  const sortedParams = Object.keys(params)
    .sort()
    .map((key) => `${percentEncode(key)}=${percentEncode(params[key])}`)
    .join("&");
  const signatureBaseString = `${httpMethod}&${baseUrl}&${percentEncode(
    sortedParams
  )}`;

  
  const consumer_secret = retoolContext.configVars.netsuite_sb_CONSUMER_SECRET
  const token_secret = retoolContext.configVars.netsuite_sb_TOKEN_SECRET

  const signature = generateSignature(
    signatureBaseString,
    consumer_secret,
    token_secret
  );

  return `OAuth realm="${retoolContext.configVars.netsuite_sb_REALM}",oauth_consumer_key="${percentEncode(
    params.oauth_consumer_key
  )}",oauth_token="${percentEncode(
    params.oauth_token
  )}",oauth_signature_method="${
    params.oauth_signature_method
  }",oauth_timestamp="${params.oauth_timestamp}",oauth_nonce="${percentEncode(
    params.oauth_nonce
  )}",oauth_version="${params.oauth_version}",oauth_signature="${percentEncode(
    signature
  )}"`;
}

return buildAuthHeader();

Give the function these 5 parameters

method
path
queryParameters
nonce
timestamp

Create another function, I called mine postNS, you'll need 1 function per method you use, ie: getNS, putNS etc., this is because you can't dynamically set the method in the function, and the method dictates the auth string. You could create a JS function that handles this more dynamically, maybe I'll try that at some point. Anyway, in this function, accept the following parameters.

auth_header
path
paylaod

Set the function up like this, configuring the method as needed and piping in the parameters
image

To use it, in a code block, do something like this:

// Set the path
const path = "query/v1/suiteql";
// Manually generate the auth
const auth = (await createNSAuth("POST", path)).data;
// Whatever your payload is, if it's a POST request
const payload = { q: "SELECT t.* FROM transaction t" };
// Send it
const result = await postNS(auth, path, payload);
return result

Not the prettiest or easiest, but it does work and allows us to leverage NetSuite data in our Retool workflows. :hand_with_index_finger_and_thumb_crossed: Retool creates a better fix one day!