phpnetsuite

Invalid login attempt. Netsuite Token-based Authentication (TBA) with REST Web Services returns InvalidSignature for query with params


When I query NS API using this URL:

https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql

It returns to me whatever I specify in my body (e.g. SELECT * FROM customer)

However, when I try to query it using this URL(it has a limit param):

https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=6

It does not work and returns the error showed down below

{
   "type":"https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2",
   "title":"Unauthorized",
   "status":401,
   "o:errorDetails":[
      {
         "detail":"Invalid login attempt. For more details, see the Login Audit Trail in the NetSuite UI at Setup > Users/Roles > User Management > View Login Audit Trail.",
         "o:errorCode":"INVALID_LOGIN"
      }
   ]
}

I suppose the problem could be either in the way I pass parameters (e.g. limit) or in the generation of the signature.

If I run this query in PostMan it works, but when it comes to PHP it does not work with any parameters I pass to Netsuite API.

If I open "Login Audit Trail Search", you can see that URL column has no ?limit=7 just like this: /services/rest/query/v1/suiteql, and the "Detail" columns says InvalidSignature

The code I am using within PHP is:

$httpMethod ="POST";
$accountID = 'NNNNNNN-sb1';
$realm = "NNNNNNN_SB1";
// this one is working fine
$url = 'https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql';
// when adding this limit it throws the error in PHP code (but postman works fine)
$url = 'https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?limit=6';
$ckey = "NNNNN"; //Consumer Key
$csecret = "NNNNN"; //Consumer Secret
$tkey = "NNNNN"; //Token ID
$tsecret = "NNNNN"; //Token Secret
$timestamp = time();
$nonce = md5(mt_rand());
$signatureMethod = 'HMAC-SHA256';
$data["q"] = "SELECT * FROM customer WHERE LENGTH(externalid) = 32";

$baseString = $httpMethod . '&' . rawurlencode($url) . '&'
    . rawurlencode(
        "oauth_consumer_key=" . rawurlencode($ckey)
        . "&oauth_nonce=" . rawurlencode($nonce)
        . "&oauth_signature_method=" . rawurlencode($signatureMethod)
        . "&oauth_timestamp=" . rawurlencode($timestamp)
        . "&oauth_token=" . rawurlencode($tkey)
        . "&oauth_version=1.0"
    );
$key = rawurlencode($csecret) . '&' . rawurlencode($tsecret);
$signature = base64_encode(hash_hmac('sha256', $baseString, $key, true));
$signature = rawurlencode($signature);
$header = array(
    "Authorization: OAuth realm=\"$realm\", oauth_consumer_key=\"$ckey\", oauth_token=\"$tkey\", oauth_nonce=\"$nonce\", oauth_timestamp=\"$timestamp\", oauth_signature_method=\"$signatureMethod\", oauth_version=\"1.0\", oauth_signature=\"$signature\"",
    'Cookie: NS_ROUTING_VERSION=LAGGING',
    'prefer: transient',
    'Content-Type: text/plain',
    'Accept: */*',
    'Content-length: ' . strlen(json_encode($data)),
);

$curl = curl_init();

$opts = array(
    CURLOPT_URL => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => "",
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 0,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => $httpMethod,
    CURLOPT_HTTPHEADER => $header,
    CURLOPT_POST => 1,
    CURLOPT_POSTFIELDS => json_encode($data)
);


curl_setopt_array($curl, $opts);

$response = curl_exec($curl);

curl_close($curl);

Screenshots (netsuite settings and postman): https://i.sstatic.net/2KtuJ.jpg

NS - netsuite


Solution

  • A friend of mine helped me really much, and we have an answer to this ambiguous question. Here is the code solution.

    1. in the base string you must only have 2 ampersands w/o encoding and no encoding on baseUrl.
    2. all params must be in alphabetical order
    <?php
    
    $httpMethod ="POST";
    $accountID = 'NNNNNNN-sb1';
    $realm = "NNNNNNN_SB1";
    // this one is working fine
    $baseUrl = 'https://'.$accountID.'.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql';
    $ckey = "NNNNN"; //Consumer Key
    $csecret = "NNNNN"; //Consumer Secret
    $tkey = "NNNNN"; //Token ID
    $tsecret = "NNNNN"; //Token Secret
    $timestamp = time();
    $nonce = md5(mt_rand());
    $signatureMethod = 'HMAC-SHA256';
    $data["q"] = "SELECT * FROM customer WHERE LENGTH(externalid) = 32";
    
    $baseString = $httpMethod . '&' . rawurlencode($baseUrl) . '&'
        . rawurlencode(
        //APPLY PARAMETERS IN ALPHABETICAL ORDER, URL ENCODED IN HERE
            "limit=2"
            . "&oauth_consumer_key=" . rawurlencode($ckey)
            . "&oauth_nonce=" . rawurlencode($nonce)
            . "&oauth_signature_method=" . rawurlencode($signatureMethod)
            . "&oauth_timestamp=" . rawurlencode($timestamp)
            . "&oauth_token=" . rawurlencode($tkey)
            . "&oauth_version=1.0"
            . "&offset=2"
        );
    $key = rawurlencode($csecret) . '&' . rawurlencode($tsecret);
    $signature = base64_encode(hash_hmac('sha256', $baseString, $key, true));
    $signature = rawurlencode($signature);
    
    $header = array(
        "Authorization: OAuth realm=\"$realm\", oauth_consumer_key=\"$ckey\", oauth_token=\"$tkey\", oauth_nonce=\"$nonce\", oauth_timestamp=\"$timestamp\", oauth_signature_method=\"$signatureMethod\", oauth_version=\"1.0\", oauth_signature=\"$signature\"",
        'Cookie: NS_ROUTING_VERSION=LAGGING',
        'prefer: transient',
        'Content-Type: text/plain',
        'Content-length: ' . strlen(json_encode($data)),
    );
    
    $curl = curl_init();
    
    $opts = array(
        CURLOPT_URL => $baseUrl . '?limit=2&offset=2', // APPLY LIMIT MANUALLY HERE
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => "",
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => $httpMethod,
        CURLOPT_HTTPHEADER => $header,
        CURLOPT_POSTFIELDS => json_encode($data)
    );
    
    
    curl_setopt_array($curl, $opts);
    
    $response = curl_exec($curl);
    
    curl_close($curl);
    var_dump($response);
    exit;