I need to write a PHP (8.2) function that calls a RESTlet on a NetSuite instance. To call this RESTlet, I'm using the Token-based Authentication (TBA) method, which in practice seems to be OAuth 1.0. All of the off-the-shelf libraries I've found for OAuth 1.0 in PHP do not use all parts of NetSuite's TBA, namely the consumer and token secrets, so I don't think they'll work. Of note, I'm not able to get the oauth PHP extension working on my machine, presumably because it was released for PHP 7.4, so I'm generating the base string portion using method shown in SOAP Web Services section. Update: I've also tried using the restletBaseString function as provided by NetSuite, but that did not work either.
As such, I'm trying to create the authorization header myself following the guide created by NetSuite, but keep getting INVALID LOGIN ATTEMPT. This is despite putting my generated headers side-by-side headers created in Postman, and them seeming to be identical. Also potentially important, when I check the Login Audit trail for the account associated with these credentials, I see no failed login attempts.
All sensitive information has been changed out, but my actual values have been directly copy/pasted from Postman where it's been working
<?php
define("ACCOUNT_ID", "1234567_SB1");
define("CONSUMER_KEY", "8daa8433f54386d63a5317d71fef801cf47a8cc3d9552be3d0f63fc8ab6de8f9");
define("CONSUMER_SECRET", "72ad5f9134091493a108a72f283733304e4218d37bc559ca99d5fd46742f7ca1");
define("TOKEN_KEY", "65d48649dcf0f8ad2da444af1d16377232bb4a5f8ffa8f3fb6175a3e6a8ed0e5");
define("TOKEN_SECRET", "bb07a68caf57fb6628661f10e687d065959cb7ab1ad6e19da1467718c67b7d6b");
define("COMPANY_URL", "https://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl");
$url = COMPANY_URL."?script=2211&deploy=1&id=28099";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: ".createOAuthHeader("GET", $url),
"Content-type: application/json",
/* Only here to test if a lack of a user-agent was the cause. It was not */
"User-Agent: Mozilla 5.0",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$empty = curl_exec($ch);
/**
* Create the OAuth header for NetSuite REST calls
*/
function createOAuthHeader(string $httpMethod = "GET", $url): string
{
$authorizor = [
"oauth_consumer_key" => CONSUMER_KEY,
"oauth_token" => TOKEN_KEY,
"oauth_signature_method" => "HMAC-SHA256",
"oauth_timestamp" => time(),
"oauth_nonce" => getOAuthNonce(),
"oauth_version" => "1.0"
];
// Create the Base String
// Failed attempt
// $base = join("&", array_map("rawurlencode", array_merge([$httpMethod, $url], flattenAssociativeArray($authorizor, false))));
// $base = oauth_get_sbs($httpMethod, $url, $authorizor);
// $base = join("&", [
// rawurlencode(ACCOUNT_ID),
// rawurlencode(CONSUMER_KEY),
// rawurlencode(TOKEN_KEY),
// rawurlencode($authorizor["oauth_nonce"]),
// rawurlencode($authorizor["oauth_timestamp"])
// ]);
// Also doesn't work
$base = restletBaseString($httpMethod, $url, CONSUMER_KEY, TOKEN_KEY, getOAuthNonce(), time(), "1.0", "HMAC-SHA256", $postParams);
// Create the HMAC signature key
$key = rawurlencode(CONSUMER_SECRET)."&".rawurlencode(TOKEN_SECRET);
// Add the base64-encoded signature to the authorizor array
$authorizor["oauth_signature"] = base64_encode(hash_hmac("sha256", $base, $key, true));
// URL encode the values in the authorizor ID
$authorizorEncoded = array_map("rawurlencode", ["realm" => ACCOUNT_ID, ...$authorizor]);
// Flattens the key/value pairs into an array of strings in the form of key="value"
$final = flattenAssociativeArray($authorizorEncoded, true);
return "OAuth ".join(",", $final);
}
function getOAuthNonce(): string
{
return substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 11);
}
function flattenAssociativeArray(array $arr, bool $quoteValue = true): array
{
$formatString = $quoteValue ? '%s="%s"' : "%s=%s";
return array_map(
fn($key, $value): string => sprintf($formatString, $key, $value)
, array_keys($arr)
, array_values($arr)
);
}
/**
* The function used to create the base string for the OAuth signature as provided by NetSuite
* @see https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534939551.html#subsect_1520632716
*/
function restletBaseString($httpMethod, $url, $consumerKey, $tokenKey, $nonce, $timestamp, $version, $signatureMethod, $postParams){
//http method must be upper case
$baseString = strtoupper($httpMethod) .'&';
//include url without parameters, schema and hostname must be lower case
if (strpos($url, '?')){
$baseUrl = substr($url, 0, strpos($url, '?'));
$getParams = substr($url, strpos($url, '?') + 1);
} else {
$baseUrl = $url;
$getParams = "";
}
$hostname = strtolower(substr($baseUrl, 0, strpos($baseUrl, '/', 10)));
$path = substr($baseUrl, strpos($baseUrl, '/', 10));
$baseUrl = $hostname . $path;
$baseString .= rawurlencode($baseUrl) .'&';
//all oauth and get params. First they are decoded, next sorted in alphabetical order, next each key and values is encoded and finally whole parameters are encoded
$params = array();
$params['oauth_consumer_key'] = array($consumerKey);
$params['oauth_token'] = array($tokenKey);
$params['oauth_nonce'] = array($nonce);
$params['oauth_timestamp'] = array($timestamp);
$params['oauth_signature_method'] = array($signatureMethod);
$params['oauth_version'] = array($version);
foreach (explode('&', $getParams ."&". $postParams) as $param) {
$parsed = explode('=', $param);
if ($parsed[0] != "") {
$value = isset($parsed[1]) ? urldecode($parsed[1]): "";
if (isset($params[urldecode($parsed[0])])) {
array_push($params[urldecode($parsed[0])], $value);
} else {
$params[urldecode($parsed[0])] = array($value);
}
}
}
//all parameters must be sorted in alphabetical order
ksort($params);
$paramString = "";
foreach ($params as $key => $valueArray){
//all values must sorted in alphabetical order
sort($valueArray);
foreach ($valueArray as $value){
$paramString .= rawurlencode($key) . '='. rawurlencode($value) .'&';
}
}
$paramString = substr($paramString, 0, -1);
$baseString .= rawurlencode($paramString);
return $baseString;
}
Example of valid authorization header created by Postman
Authorization: OAuth realm="1234567_SB1"
,oauth_consumer_key="8daa8433f54386d63a5317d71fef801cf47a8cc3d9552be3d0f63fc8ab6de8f9"
,oauth_token="65d48649dcf0f8ad2da444af1d16377232bb4a5f8ffa8f3fb6175a3e6a8ed0e5"
,oauth_signature_method="HMAC-SHA256"
,oauth_timestamp="1710773563"
,oauth_nonce="qcFwZvbnJ5u"
,oauth_version="1.0"
,oauth_signature="iL7L430GE%2Bdw5oSBISr848VKNFm5LLsMskiMD362qCk%3D"
Example of invalid authorization header created by PHP
Authorization: OAuth realm="1234567_SB1"
,oauth_consumer_key="8daa8433f54386d63a5317d71fef801cf47a8cc3d9552be3d0f63fc8ab6de8f9"
,oauth_token="65d48649dcf0f8ad2da444af1d16377232bb4a5f8ffa8f3fb6175a3e6a8ed0e5"
,oauth_signature_method="HMAC-SHA256"
,oauth_timestamp="1710775464"
,oauth_nonce="3Hulo6OPf78"
,oauth_version="1.0"
,oauth_signature="zQlMYe%2F9VtANAvgvJo%2Bs7SvLMTvcrrJcOa8Yz5wLQsM%3D"
The comments provided by @Brian were correct, in that I was not originally signing enough values, and the values I was signing were not alphabetically sorted by their key. Furthermore, when trying to use the
restletBaseStringfunction provided by NetSuite, I was generating a new nonce and a new timestamp to create the base string, and then still using the nonce and timestamp I created previously in the$authorizorarray when creating the headers. This caused a mismatch of values between the signature and the headers.