OAuth 1.0 PHP 代码到 netsuite(在 Postman 中工作)

问题描述 投票:0回答:1

使用这两个问题: 使用 PHP 的 Netsuite Rest OAuthOAuth 1.0 生成签名与 Postman 生成签名 作为参考,我写了这段代码

    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;

如您所见,签名不包括领域,但我不断收到:

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."

错误消息: OAuth 领域 =“xyz”,错误 =“token_rejected”,error_description =“无效的登录尝试。”

但 POSTMAN 中似乎也有同样的功能。 有人看到我做错了什么吗?

*编辑 以下是完整回复的示例:

*   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"}]}
"

更新 我找到了为什么它无效,因为错误消息详细信息显示:InvalidSignature

但我不知道为什么它无效或者我做错了什么。

更多参考:https://gist.github.com/britbarn/cb8d2e6a27a54634418028d6c941c604 https://github.com/netsuitephp/netsuite-php

php oauth netsuite
1个回答
0
投票

我做了更多研究,我的 $baseString 不正确。 值得庆幸的是,Oracle 在 https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534939551.html#subsect_1521030602

上发布了一个 RestletBaseString 函数

这解决了问题。

//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;
}

电话看起来像这样:

        $baseString = $this->restletBaseString('GET', $this->getRequestUrl(), $consumerKey, $token, $nonce, $timestamp, $version, $sigMethod);
    $key = $consumerSecret . '&' . $tokenSecret;

最终代码如下所示:

  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 = $consumerSecret . '&' . $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;
}
© www.soinside.com 2019 - 2024. All rights reserved.