Reputation: 7289
Trying to use/call a NetSuite Restlet with token based authentication using the Delphi RESTClient. I have a working Postman request that verifies all the authorization parameters required. The one parameter missing - realm - when added in code does change the error message returned from the NetSuite side but still doesn't result in a successful call.
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;
If realm is not added (the above code is commented out) the returned rest content is:
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" ]
With realm added I still get:
error code: INVALID_LOGIN_ATTEMPT
error message: Invalid login attempt.
Anybody successfully make calls to NetSuite restlets using the Delphi RESTClient?
Update: Looks like it does not support using the optional realm parameter. Adding it manually gets it added to the normalized parameter list for signing when it shouldn't be. I have modified TOAuth1SignatureMethod_HMAC_SHA1.BuildSignature
in REST.Authenticator.OAuth
to skip this parameter (just added a if Lparam.Name <> 'realm' then begin .. end;
block around some code) but still no luck successfully making a request to NetSuite. Did this after reading https://oauth.net/core/1.0/ section 9.1.1. Normalize Request Parameters:
The request parameters are collected, sorted and concatenated into a normalized string:
- Parameters in the OAuth HTTP Authorization header excluding the realm parameter.
- Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded).
- HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3).
Upvotes: 3
Views: 1631
Reputation: 6455
I have created a new Authenticator on Delphi 11, derived from TOAuth1Authenticator, with support for SHA256 and the Realm header.
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.
There is a bit of a hack to access a private method using a Helper.
This is an example of use:
function TdmIntegrationNetSuite.GetCustomer(Id: string): string;
begin
var LClient := TRESTClient.Create(Self);
var LRequest := TRESTRequest.Create(Self);
var LResponse := TRESTResponse.Create(Self);
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;
LClient.Authenticator := LAuthenticator;
LRequest.Client := LClient;
LRequest.Response := LResponse;
LClient.BaseURL := 'https://XXXXXXXX.suitetalk.api.netsuite.com/services/rest/record/v1';
LRequest.Resource := 'customer/' + Id;
LRequest.Execute;
Result := LResponse.Content;
end;
Upvotes: 2
Reputation: 7289
Ended up manually assembling the request with Indy. A copy of the part that assembles the AUTH header is below along with code that does a test request using Indy's IdHTTP. Note: the Netsuite account number and IDs/Secrets are just for show.
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;
Note: code updated to use SHA256 instead of SHA1 for OAUTH signing. NetSuite has sent out notice they will be requiring this with NetSuite Release 2021.2.
Upvotes: 4