Using these 2 questions: Netsuite Rest OAuth using PHP and OAuth 1.0 Generate Signature vs Postman Generate Signature for reference, I wrote this code
public function getValueFromOAuth(string $headerValue, string $key): ?string{
if (preg_match('/'.preg_quote($key).'="(.*?)"/i', $headerValue, $matches)){
return $matches[1];
}else{
return null;
}
}
public function getOAuthHeaderValue(string $key, string $value): string{
if (strtolower($key)!=='authorization'){
return $value;
}
if (strpos($value,'OAuth') === false){
return $value;
}
$value = str_replace(""", '"', $value);
$rebuildOauth1 = [];
$consumerKey = $this->getValueFromOAuth($value, 'oauth_consumer_key');
//$nonce = $this->getValueFromOAuth($value, 'oauth_nonce');
$sigMethod = $this->getValueFromOAuth($value, 'oauth_signature_method');
//$timestamp = $this->getValueFromOAuth($value, 'oauth_timestamp');
$token = $this->getValueFromOAuth($value, 'oauth_token');
$version = $this->getValueFromOAuth($value, 'oauth_version');
$realm = $this->getValueFromOAuth($value, 'realm');
$timestamp = time();
$nonce = mb_substr(md5(mt_rand()),0 , 11);
$consumerSecret = $this->getValueFromOAuth($value, 'oauth_consumer_secret');
$tokenSecret = $this->getValueFromOAuth($value, 'oauth_token_secret');
$rebuildOauth1 []= 'oauth_consumer_key="'.$consumerKey.'"';
if ($nonce) {
$rebuildOauth1 [] = 'oauth_nonce="' . $nonce . '"';
}
if (!is_null($sigMethod)) {
$rebuildOauth1 [] = 'oauth_signature_method="' . $sigMethod . '"';
}
if ($timestamp) {
$rebuildOauth1 [] = 'oauth_timestamp="' . $timestamp . '"';
}
if ($token){
$rebuildOauth1[] = 'oauth_token="'.$token.'"';
}
if (!is_null($version)) {
$rebuildOauth1 [] = 'oauth_version="' . $version . '"';
}
$rebuildOauth1Str = implode(',',$rebuildOauth1);
$baseString = strtoupper($this->getRequestType()).'&'.rawurlencode($this->getRequestUrl()).'&'.rawurlencode($rebuildOauth1Str);
$sigString = urlencode($consumerSecret).'&'.urlencode($tokenSecret);
$signature = base64_encode(hash_hmac("sha256", $baseString, $sigString, true));
if (!is_null($realm)) {
$rebuildOauth1 [] = 'realm="' . $realm . '"';
$rebuildOauth1Str = implode(',',$rebuildOauth1);
}
$rebuiltOauthHeader = 'OAuth oauth_signature="'.$signature.'",'.
$rebuildOauth1Str;
return $rebuiltOauthHeader;
As you can see, the signature is NOT including the realm, but I keep getting:
authorization: OAuth oauth_signature="xyz=",oauth_consumer_key="xyz",oauth_nonce="xyz",oauth_signature_method="HMAC-SHA256",oauth_timestamp="1699465443",oauth_token="xyz",oauth_version="1.0",realm="xyz"
connection: keep-alive
accept: */*
content-type: application/json; charset=UTF-8
* old SSL session ID is stale, removing
< HTTP/2 401
< content-type: application/vnd.oracle.resource+json; type=error; charset=UTF-8
< content-length: 321
...
< www-authenticate: OAuth realm="xyz", error="token_rejected", error_description="Invalid login attempt."
Error Message: OAuth realm="xyz", error="token_rejected", error_description="Invalid login attempt."
But it seems that the same thing is working in POSTMAN. Does anyone see what I am doing wrong?
*EDIT Here is an example of the full response:
* Trying 123.123.12.1:443...
* Connected to 123456-sb1.suitetalk.api.netsuite.com (104.114.79.8) port 443 (#0)
* ALPN: offers h2,http/1.1
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted h2
* Server certificate:
* subject: C=US; ST=California; L=Redwood City; O=Oracle Corporation; CN=extforms.netsuite.com
* start date: Jan 23 00:00:00 2023 GMT
* expire date: Jan 23 23:59:59 2024 GMT
* subjectAltName: host "123456-sb1.suitetalk.api.netsuite.com" matched cert's "*.suitetalk.api.netsuite.com"
* issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS RSA SHA256 2020 CA1
* SSL certificate verify ok.
* using HTTP/2
* h2h3 [:method: GET]
* h2h3 [:path: /services/rest/record/v1/purchaseOrder/?q=custbody15+IS+TRUE&limit=1]
* h2h3 [:scheme: https]
* h2h3 [:authority: 123456-sb1.suitetalk.api.netsuite.com]
* h2h3 [user-agent: P Application/2]
* h2h3 [authorization: OAuth oauth_signature="ABC123=",oauth_consumer_key="123",oauth_nonce="518cf0608cd",oauth_signature_method="HMAC-SHA256",oauth_timestamp="1699471397",oauth_token="456",oauth_version="1.0",realm="123456_SB1"]
* h2h3 [accept: */*]
* h2h3 [content-type: application/json; charset=UTF-8]
* Using Stream ID: 1 (easy handle 0xaaaad63edc70)
> GET /services/rest/record/v1/purchaseOrder/?q=custbody15+IS+TRUE&limit=1 HTTP/2
Host: 123456-sb1.suitetalk.api.netsuite.com
user-agent: P Application/2
authorization: OAuth oauth_signature="ABC123=",oauth_consumer_key="123",oauth_nonce="518cf0608cd",oauth_signature_method="HMAC-SHA256",oauth_timestamp="1699471397",oauth_token="456",oauth_version="1.0",realm="123456_SB1"
connection: keep-alive
accept: */*
content-type: application/json; charset=UTF-8
* old SSL session ID is stale, removing
< HTTP/2 401
< content-type: application/vnd.oracle.resource+json; type=error; charset=UTF-8
< content-length: 321
< x-n-operationid: 123
< ns_rtimer_composite: 123:123:80
< strict-transport-security: max-age=31536000
< pragma: No-Cache
< cache-control: No-Cache
< expires: 0
< www-authenticate: OAuth realm="123456_SB1", error="token_rejected", error_description="Invalid login attempt."
< vary: User-Agent
< date: Wed, 08 Nov 2023 19:23:19 GMT
< akamai-grn: 0.123.456.789a
<
* Connection #0 to host 123456-sb1.suitetalk.api.netsuite.com left intact
string(920) "HTTP/2 401
content-type: application/vnd.oracle.resource+json; type=error; charset=UTF-8
content-length: 321
x-n-operationid: 123
ns_rtimer_composite: 123:123:80
strict-transport-security: max-age=31536000
pragma: No-Cache
cache-control: No-Cache
expires: 0
www-authenticate: OAuth realm="123456_SB1", error="token_rejected", error_description="Invalid login attempt."
vary: User-Agent
date: Wed, 08 Nov 2023 19:23:19 GMT
akamai-grn: 0.123.456.789a
{"type":"https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.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"}]}
"
UPDATE I found out why it is invalid, because the Error message Detail says: InvalidSignature
But I do not know why it is invalid or what I am doing wrong.
More references: https://gist.github.com/britbarn/cb8d2e6a27a54634418028d6c941c604 https://github.com/netsuitephp/netsuite-php
I did some more research, and my $baseString was not correct. Thankfully, Oracle posted a restletBaseString function on https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534939551.html#subsect_1521030602
Which solved the problem.
//https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534939551.html#subsect_1521030602
public 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 be 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;
}
And the call looked like this:
$baseString = $this->restletBaseString('GET', $this->getRequestUrl(), $consumerKey, $token, $nonce, $timestamp, $version, $sigMethod);
$key = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret);
The Final code looks like this:
public function getOAuthHeaderValue(string $key, string $value): string{
if (strtolower($key)!=='authorization'){
return $value;
}
if (strpos($value,'OAuth') === false){
return $value;
}
$origValue = $value;
$value = str_replace(""", '"', $value);
$rebuildOauth1 = [];
$consumerKey = $this->getValueFromOAuth($value, 'oauth_consumer_key');
//$nonce = $this->getValueFromOAuth($value, 'oauth_nonce');
$sigMethod = $this->getValueFromOAuth($value, 'oauth_signature_method');
//$timestamp = $this->getValueFromOAuth($value, 'oauth_timestamp');
$token = $this->getValueFromOAuth($value, 'oauth_token');
$version = $this->getValueFromOAuth($value, 'oauth_version');
$realm = $this->getValueFromOAuth($value, 'realm');
if (empty($realm)){
return $origValue;
}
$timestamp = time();
$nonce = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 11);
//mb_substr(md5(mt_rand()),0 , 11);
$consumerSecret = $this->getValueFromOAuth($value, 'oauth_consumer_secret');
$tokenSecret = $this->getValueFromOAuth($value, 'oauth_token_secret');
//We will rebuild alphabetically so that the signature is valid.
$rebuildOauth1 []= 'oauth_consumer_key="'.rawurlencode($consumerKey).'"';
if ($nonce) {
$rebuildOauth1 []= 'oauth_nonce="' . rawurlencode($nonce) . '"';
}
if (!is_null($sigMethod)) {
$rebuildOauth1 []= 'oauth_signature_method="' . rawurlencode($sigMethod) . '"';
}
if ($timestamp) {
//Timestamp could be off? https://stackoverflow.com/a/67677999/1993494
$rebuildOauth1 []= 'oauth_timestamp="' . rawurlencode($timestamp) . '"';
}
if ($token){
$rebuildOauth1 []= 'oauth_token="'.rawurlencode($token).'"';
}
if (!is_null($version)) {
$rebuildOauth1 [] = 'oauth_version="' . rawurlencode($version) . '"';
}
$rebuildOauth1Str = implode(',',$rebuildOauth1);
$baseString = $this->restletBaseString('GET', $this->getRequestUrl(), $consumerKey, $token, $nonce, $timestamp, $version, $sigMethod);
$key = rawurlencode($consumerSecret) . '&' . rawurlencode($tokenSecret);
$signature = base64_encode(hash_hmac("sha256", $baseString, $key, true));
$rebuiltOauthHeader = 'OAuth ';
if (!is_null($realm)) {
$rebuiltOauthHeader .= 'realm="' . $realm . '",';
}
$rebuiltOauthHeader .= $rebuildOauth1Str.',oauth_signature="' . $signature . '"';
return $rebuiltOauthHeader;
}