Search code examples
phpoauthnetsuite

OAuth 1.0 PHP code to netsuite (works in Postman)


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


Solution

  • 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("&quot;", '"', $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;
    }