Delphi TOAuth1Authenticator如何设置Realm

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

尝试使用 Delphi RESTClient 使用/调用带有基于令牌的身份验证的 NetSuite Restlet。我有一个有效的 Postman 请求,用于验证所需的所有授权参数。缺少一个参数 - 领域 - 当在代码中添加时,确实会更改从 NetSuite 端返回的错误消息,但仍然不会导致成功调用。

procedure TForm1.OAuth1Authenticator1Authenticate(ARequest: TCustomRESTRequest;
    var ADone: Boolean);
begin
  // XXXXXXX_RP is a NetSuite account number and environment 
  ARequest.AddAuthParameter('realm','XXXXXX_RP',pkQUERY);  
end;

如果不添加realm(上面代码被注释掉)则返回的剩余内容为:

error code: USER_ERROR
error message: header is not NLAuth scheme [ OAuth oauth_consumer_key="fe8da7b31dccbd47e90e5dd5e641fe7b0cbff032a951b4058618c207caf569f8", oauth_nonce="c98fa9de2c601f45bdc8d5c640f1b3cf", oauth_signature_method="HMAC-SHA1", oauth_signature="PfCQE3A4DicTtCfpBEPmbSOmqNg%3D", oauth_timestamp="1568639138", oauth_token="a8190ba34e223f25b32cdb4837d9e1973b8fd6208804e93306f4618ccdb6d648", oauth_version="1.0" ]

添加领域后我仍然得到:

error code: INVALID_LOGIN_ATTEMPT
error message: Invalid login attempt.

有人使用 Delphi RESTClient 成功调用 NetSuite Restlet 吗?

更新:看起来它不支持使用可选的realm参数。手动添加它会将其添加到规范化参数列表中,以便在不应该进行签名时进行签名。我已经修改了

TOAuth1SignatureMethod_HMAC_SHA1.BuildSignature
中的
REST.Authenticator.OAuth
以跳过此参数(只是在一些代码周围添加了
if Lparam.Name <> 'realm' then begin .. end;
块),但仍然没有成功向 NetSuite 发出请求。在阅读https://oauth.net/core/1.0/第9.1.1节后这样做。规范化请求参数:

请求参数被收集、排序并串联成一个 标准化字符串:

  • OAuth HTTP 授权标头中的参数不包括领域参数
  • HTTP POST 请求正文中的参数(内容类型为 application/x-www-form-urlencoded)。
  • HTTP GET 参数添加到查询部分的 URL(如 [RFC3986] 第 3 节所定义)。
delphi oauth netsuite delphi-10.3-rio
2个回答
4
投票

最终通过 Indy 手动组装请求。下面是组装 AUTH 标头的部分的副本,以及使用 Indy 的 IdHTTP 执行测试请求的代码。注意:Netsuite 帐号和 ID/Secret 仅供展示。

procedure TForm1.Button1Click(Sender: TObject);
Const
  NETSUITE_ACCOUNT_ID = '4000000_RP';
  BASE_URL = 'https://4000000-rp.restlets.api.netsuite.com/app/site/hosting/restlet.nl';
  HTTP_METHOD = 'POST';
  SCRIPT_ID = '331';
  SCRIPT_DEPLOYMENT_ID = 'customdeploy_salesorderimport';
  OAUTH_VERSION = '1.0';
  TOKEN_ID = 'a8190ba34e223f25b3267843437d9e1973b8fd6208804e93306f4618ccdb6d648';
  TOKEN_SECRET = 'ecb5321eaf832714828ede7f920320b942ad6b2d4221da3b12496f389d68e1c4';
  CONSUMER_KEY = 'fe8da7c2cbd47e90e5dd5e6415fe7b0cbff2032a5951b4058618c07c7af569f8';
  CONSUMER_SECRET = '848cae6150a651ecbc6975656f4b92ca08d7c27c829185cf98e7a0a30c24dbc2';
Var
  OAUTH_NONCE,TIME_STAMP,STRING2SIGN : string;
  oauth_signature, oauth, BaseURL : string;
  JSONValue : TJsonValue;
begin
  OAUTH_NONCE :=  THashMD5.GetHashString(IntToStr(DateTimeToUnix(TTimeZone.Local.ToUniversalTime(Now))) + IntToStr(Random(MAXINT)));
  TIME_STAMP := IntToStr(DateTimeToUnix(TTimeZone.Local.ToUniversalTime(Now)));

  // These are in alphabetical order - required by signing
  STRING2SIGN := '';
  STRING2SIGN := STRING2SIGN + 'deploy=' + SCRIPT_DEPLOYMENT_ID + '&';
  STRING2SIGN := STRING2SIGN + 'oauth_consumer_key=' + CONSUMER_KEY + '&';
  STRING2SIGN := STRING2SIGN + 'oauth_nonce=' + OAUTH_NONCE + '&';
  STRING2SIGN := STRING2SIGN + 'oauth_signature_method=' + 'HMAC-SHA256' + '&';
  STRING2SIGN := STRING2SIGN + 'oauth_timestamp=' + TIME_STAMP + '&';
  STRING2SIGN := STRING2SIGN + 'oauth_token=' + TOKEN_ID + '&';
  STRING2SIGN := STRING2SIGN + 'oauth_version=' + OAUTH_VERSION + '&';
  STRING2SIGN := STRING2SIGN + 'script=' + SCRIPT_ID;
  STRING2SIGN := URIEncode(STRING2SIGN);
  STRING2SIGN := HTTP_METHOD + '&' + URIEncode(BASE_URL) + '&' + STRING2SIGN;
  oauth_signature := URIEncode(TNetEncoding.Base64.EncodeBytesToString(THashSHA2.GetHMACAsBytes(STRING2SIGN, CONSUMER_SECRET + '&' + TOKEN_SECRET)));

  oauth :='OAuth oauth_signature="' + oauth_signature + '",';
  oauth := oauth + 'oauth_version="' + OAUTH_VERSION + '",';
  oauth := oauth + 'oauth_nonce="' + OAUTH_NONCE + '",';
  oauth := oauth + 'oauth_signature_method="HMAC-SHA256",';
  oauth := oauth + 'oauth_consumer_key="' + CONSUMER_KEY + '",';
  oauth := oauth + 'oauth_token="' + TOKEN_ID + '",';
  oauth := oauth + 'oauth_timestamp="' + TIME_STAMP + '",';
  oauth := oauth + 'realm="' + NETSUITE_ACCOUNT_ID + '"';

  BaseURL := BASE_URL + '?script=' + SCRIPT_ID + '&deploy=' + SCRIPT_DEPLOYMENT_ID;

  IdHTTP1.Request.CustomHeaders.FoldLines := false;
  IdHTTP1.Request.Accept := 'application/json, text/javascript, */*; q=0.01';
  IdHTTP1.Request.ContentType := 'application/json';
  IdHTTP1.Request.CustomHeaders.Values['Authorization'] := oauth;
try
  memo1.Text := idhttp1.Post(BaseURL,'C:\Users\bevans\Documents\Json.txt');
  JSONValue :=  TJSonObject.ParseJSONValue(memo1.Text);
  SynEdit1.Text := JSonValue.Format;
finally
  JSonValue.Free;
end;

注意:代码已更新为使用 SHA256 而不是 SHA1 进行 OAUTH 签名。 NetSuite 已发出通知,他们将在 NetSuite 版本 2021.2 中要求这样做。


0
投票

我在 Delphi 11 上创建了一个新的 Authenticator,它派生自 TOAuth1Authenticator,支持 SHA256 和 Realm 标头。

unit Custom.Authenticator.OAuth;

interface

uses
  System.Classes,
  REST.Authenticator.OAuth,
  REST.Client;

type

  TOAuth1AuthenticatorHelper = class helper for TOAuth1Authenticator
    procedure AddCommonAuthParametersHelper(const Params: TStrings; const QuoteChar: string);
  end;

  TOAuth1SignatureMethod_HMAC_SHA256 = class(TOAuth1SignatureMethod)
  protected
    function Hash_HMAC_SHA256(const AData, AKey: string): string; virtual;
  public
    class function GetName: string; override;
    function BuildSignature(ARequest: TCustomRESTRequest; AAuthenticator: TOAuth1Authenticator): string; override;
  end;

  TKpOAuth1Authenticator = class(TOAuth1Authenticator)
  private
    FRealm: string;
  protected
    procedure DoAuthenticate(ARequest: TCustomRESTRequest); override;
  public
    property Realm: string read FRealm write FRealm;
  end;

implementation

uses
  System.SysUtils,
  System.Hash,
  System.NetEncoding,
  REST.Types,
  REST.Utils,
  REST.Consts;

procedure TOAuth1AuthenticatorHelper.AddCommonAuthParametersHelper(const Params: TStrings; const QuoteChar: string);
var
  P: procedure(const Params: TStrings; const QuoteChar: string) of object;
begin
  // Hack per a accedir al mètode privat AddCommonAuthParameters en la classe TOAuth1Authenticator de REST.Authenticator.OAuth
  System.TMethod(P).Code := @TOAuth1Authenticator.AddCommonAuthParameters;
  System.TMethod(P).Data := Self;
  P(Params, QuoteChar);
end;

function TOAuth1SignatureMethod_HMAC_SHA256.Hash_HMAC_SHA256(const AData, AKey: string): string;
begin
  Result := TNetEncoding.Base64.EncodeBytesToString(THashSHA2.GetHMACAsBytes(AData, AKey));
end;

class function TOAuth1SignatureMethod_HMAC_SHA256.GetName: string;
begin
  Result := 'HMAC-SHA256'; // do not localize
end;

function TOAuth1SignatureMethod_HMAC_SHA256.BuildSignature(ARequest: TCustomRESTRequest; AAuthenticator: TOAuth1Authenticator): string;
var
  LPayLoadParams: TRESTRequestParameterArray;
  LParamList: TStringList;
  LURL: string;
  LParamsStr: string;
  LSigBaseStr: string;
  LSigningKey: string;
  LParam: TRESTRequestParameter;
begin
  Assert(Assigned(ARequest) and Assigned(AAuthenticator));

  Result := '';

  // This code is duplicated.  Use common representation of name/value pairs.  Don't build the same list in two places.

  // Step #1 - collect all relevant parameters, this includes
  // all oauth_params as well as the params from the payload
  // (payload-params ==> params from client and request)
  LParamList := TStringList.Create;
  try
    AAuthenticator.AddCommonAuthParametersHelper(LParamList, '');
    // now collect the parameters from the payload. we do need the
    // union of the client-parameters and the request-parameters
    LPayLoadParams := ARequest.CreateUnionParameterList;
    for LParam in LPayLoadParams do
      if LParam.Kind in [TRESTRequestParameterKind.pkGETorPOST,
                         TRESTRequestParameterKind.pkQUERY] then
        if poDoNotEncode in LParam.Options then
          LParamList.Values[LParam.Name] := LParam.Value
        else
          LParamList.Values[LParam.Name] := URIEncode(LParam.Value);

    // Step #2 - build a single string from the params
    // OAuth-spec requires the parameters to be sorted by their name
    LParamList.Sort;
    LParamList.LineBreak := '&';
    LParamList.Options := LParamList.Options - [soTrailingLineBreak];
    LParamsStr := LParamList.Text;
  finally
    LParamList.Free;
  end;

  // as per oauth-spec we do need the full URL without (!) any query-params
  LURL := ARequest.GetFullRequestURL(FALSE);

  // Step #3 - build the SignatureBaseString, the LSigningKey and the Signature
  LSigBaseStr := UpperCase(RESTRequestMethodToString(ARequest.Method)) + '&' +
    URIEncode(LURL) + '&' +
    URIEncode(LParamsStr); // do not localize

  LSigningKey := AAuthenticator.ConsumerSecret + '&';

  if AAuthenticator.AccessTokenSecret <> '' then
    LSigningKey := LSigningKey + AAuthenticator.AccessTokenSecret // do not localize
  else if AAuthenticator.RequestTokenSecret <> '' then
    LSigningKey := LSigningKey + AAuthenticator.RequestTokenSecret; // do not localize

  Result := Hash_HMAC_SHA256(LSigBaseStr, LSigningKey);
end;

procedure TKpOAuth1Authenticator.DoAuthenticate(ARequest: TCustomRESTRequest);
begin
  inherited DoAuthenticate(ARequest);

  if FRealm <> '' then
  begin
   if Assigned(ARequest.Params.ParameterByName(HTTP_HEADERFIELD_AUTH)) then
     ARequest.Params.ParameterByName(HTTP_HEADERFIELD_AUTH).Value := ARequest.Params.ParameterByName(HTTP_HEADERFIELD_AUTH).Value + ',realm="' + FRealm + '"';

   if Assigned(ARequest.TransientParams.ParameterByName(HTTP_HEADERFIELD_AUTH)) then
     ARequest.TransientParams.ParameterByName(HTTP_HEADERFIELD_AUTH).Value := ARequest.TransientParams.ParameterByName(HTTP_HEADERFIELD_AUTH).Value + ',realm="' + FRealm + '"';
  end
end;

end.

使用 Helper 访问私有方法需要一些技巧。

这是一个使用示例:

function TdmIntegrationNetSuite.GetCustomer(Id: string): string;
begin
  var LAuthenticator := TKpOAuth1Authenticator.Create(Self);
  LAuthenticator.ConsumerKey := 'XXXXXXXXXXXXXXXXXXXXXXXXX';
  LAuthenticator.ConsumerSecret := 'XXXXXXXXXXXXXXXXXXXXXXXXX';
  LAuthenticator.AccessToken := 'XXXXXXXXXXXXXXXXXXXXXXXXX';
  LAuthenticator.AccessTokenSecret := 'XXXXXXXXXXXXXXXXXXXXXXXXX';
  LAuthenticator.Realm := 'XXXXXXXX';
  LAuthenticator.SigningClass := TOAuth1SignatureMethod_HMAC_SHA256.Create;

  RESTClient1.Authenticator := LAuthenticator;
  RESTClient1.BaseURL := 'https://XXXXXXXX.suitetalk.api.netsuite.com/services/rest/record/v1';
  RESTRequest1.Resource := 'customer/' + Id.ToString;
  RESTRequest1.Execute;
  Result := RESTResponse1.Content;
end;
© www.soinside.com 2019 - 2024. All rights reserved.