Friday, 11 October 2019

MuleSoft to NetSuite-SuiteTalk Token Based Authentication


Overview

In this blog post, I’ll walk you through consuming SOAP WS (SuiteTalk) using Token Based Authentication in NetSuite for integration from MuleSoft in particular, as I am providing the code for this, however, the same principle can be applied to any integration platform or the same can be implemented in simple Java code.

NetSuite’s OAuth is very different from the standard OAuth and, although MuleSoft has a very good connector for it (see Mule to Netsuite connector) sometimes it is good to use the SuiteTalk WS because you'll get full control and visibility of all the input and output parameters.
Additionally, I personally used the WS approach as the connector was published after our development was completed.

Unfortunately, this configuration is not the easiest as it requires some custom coding to make it working in MuleSoft.

For my examples and code, I used the below configuration:


Netsuite suite Talk
2018.2
Anypoint Studio
7.3
CloudHub Runtime
4.1.5

Additionally, in my examples, I am calling the operation Search, by predefined SearchID, however by changing the body to the expected one all the operations can be called.

How to set up token-based authentication in Netsuite

Token’s provide a secure authentication mechanism to connect to NetSuite without using the standard username and password and most importantly for integrations they do not expire when the credentials are changed or the password expires.

I am not going into the details of this configuration as I am not an expert for that and so I will point you to some good article that explains that:
https://support.cazoomi.com/hc/en-us/articles/360010093392-How-to-Setup-NetSuite-Token-Based-Authentication-as-Authentication-Type

How to use the Search operation via SOAP SuiteTalk Netsuite

  • Download the WSDL and all related Schama XSD by using the following link

You will need to download all the directly imported schema, plus any referenced schema within other XSDs and then make sure relative path (xsd:import) for the imports are set correctly.

I always download WSDL and XSD locally so that I do not need to wait for downloads every time Anypoint has to validate the WS configurations.

  • Create the Netsuite SuiteTalk WS Config in the API Config Flow,  as per below example by using properties file for endpoint URL which will vary by the Netsuite target environment

<wsc:config name="Web_Service_Consumer_Config" doc:name="Web Service Consumer Config" >
  <wsc:connection wsdlLocation="schemas/netsuite.wsdl" service="NetSuiteService" port="NetSuitePort" address="${netsuite.endpoint.url}">
    <wsc:custom-transport-configuration >
    <wsc:http-transport-configuration requesterConfig="NetsuiteHTTPConfiguration" />
    </wsc:custom-transport-configuration>
  </wsc:connection>
</wsc:config>


  • Create the SuiteTalk Search Body via DW, in the API Implementation Flow, as per below examples (by using properties file for searchID which will vary by environment)

Example1: Search Body without filters for a predefined SearchID of type ItemSearchAdvanced:

<ee:transform doc:name="Create query without filter">
<ee:message >
<ee:set-payload ><![CDATA[%dw 2.0
output application/xml
ns urn urn:messages_2018_2.platform.webservices.netsuite.com
ns urn1 urn:core_2018_2.platform.webservices.netsuite.com
ns ns28 urn:accounting_2018_2.lists.webservices.netsuite.com
ns ns38 urn:common_2018_2.platform.webservices.netsuite.com
ns xsi http://www.w3.org/2001/XMLSchema-instance
---
{
    urn#search: {
        urn#searchRecord @(xsi#'type': "q1:ItemSearchAdvanced", savedSearchId:p("somesearch.id"), 'xmlns:q1':"urn:accounting_2018_2.lists.webservices.netsuite.com") : {
        }
    }
        
}]]></ee:set-payload>
</ee:message>
</ee:transform>

Example2: Search Body with a filter on a custom attribute for a predefined SearchID of type ItemSearchAdvanced:


<ee:transform doc:name="Create query with filter">
<ee:message >
<ee:set-payload ><![CDATA[%dw 2.0
output application/xml
ns urn urn:messages_2018_2.platform.webservices.netsuite.com
ns urn1 urn:core_2018_2.platform.webservices.netsuite.com
ns ns28 urn:accounting_2018_2.lists.webservices.netsuite.com
ns ns38 urn:common_2018_2.platform.webservices.netsuite.com
ns xsi http://www.w3.org/2001/XMLSchema-instance
---
{
    urn#search: {
        urn#searchRecord @(xsi#'type': "q1:ItemSearchAdvanced", savedSearchId:p("somesearch.id"), 'xmlns:q1':"urn:accounting_2018_2.lists.webservices.netsuite.com") : {
            ns28#criteria: {
                ns28#basic: {
                    ns38#customFieldList: {
                        urn1#customField @(scriptId:"somecustomattribute", xsi#'type':"urn1:SearchLongCustomField", operator:"greaterThan") : {
                            urn1#searchValue: vars.maxReadDate
                        }
                    }
                }
            }
        }
    }
        
}]]></ee:set-payload>
</ee:message>
</ee:transform>

  • Generate the Netsuite SuiteTalk Token-Based Authentication headers, in the same API Implementation flow, as below
<set-variable value='#[%dw 2.0
import java!com::mule::utils::CustomUtils
output application/xml
ns urn urn:messages_2018_2.platform.webservices.netsuite.com
ns urn1 urn:core_2018_2.platform.webservices.netsuite.com

var nonce = CustomUtils::getAuthNonce()
var timestamp = CustomUtils::getAuthTimestamp()
var accountId = p("login.netsuite.user.account.id")
var comnsumerKey = p("login.netsuite.consumer.key")
var tokenId = p("login.netsuite.token.id")
var consumerSecret = p("login.netsuite.consumer.secret")
var tokenSecret = p("login.netsuite.token.secret")

var signature = CustomUtils::generateAuthSignature(accountId, comnsumerKey, tokenId, consumerSecret, tokenSecret, nonce, timestamp)
---
headers: {
    urn#searchPreferences: {
    urn#bodyFieldsOnly: false,
    urn#returnSearchColumns: true,
    urn#pageSize: p("products.netsuite.search.pagesize")
    },
    urn#tokenPassport: {
    urn1#account: accountId,
    urn1#consumerKey: comnsumerKey,
    urn1#token: tokenId,
    urn1#nonce: nonce,
    urn1#timestamp: timestamp,
    urn1#signature @(algorithm:"HMAC-SHA1"): signature
    }
    }]' doc:name="Set Headers" variableName="requestHeaders" />

See at the end of the post the java class used to generate the headers. 

  • Call the Netsuite SuiteTalk Search operation, in the same API Implementation flow, as below
<wsc:consume doc:name="Consume" config-ref="Web_Service_Consumer_Config" operation="search">
<reconnect />
<wsc:message>
<wsc:headers><![CDATA[#[vars.requestHeaders]]]></wsc:headers>
</wsc:message>
</wsc:consume>

I hope you find this information useful in your next Integration project!
Please feel free to share your NetSuite integration tips and feedback on this article.

CustomUtils Java class

package com.mule.utils;

import java.util.Arrays;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class CustomUtils {

private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
public static String getAuthNonce() {
//return "" + (System.currentTimeMillis() / 1000L);
int nonceLength = 20;
String chars =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String result = "";
for (int i = nonceLength; i > 0; --i) 
result += chars.charAt((int) Math.floor(Math.
random() * chars.length()));
return result;
}

public static String getAuthTimestamp() {
return "" + (System.currentTimeMillis() / 1000L);
}
public static String generateAuthSignature(String accountId, String consumerKey, String tokenId,
String consumerSecret, String tokenSecret, String nonce, String timeStamp)
throws InvalidKeyException, SignatureException, NoSuchAlgorithmException {
String signature = "";
String concatenatedSecrets = "";

String  timeMillis = "" + System.currentTimeMillis();
signature = accountId + "&" + consumerKey + "&" + tokenId + "&" + nonce + "&" + timeStamp;

// Sign the result string from step 5 using the consumer secret and token secret
// concatenated using '&' (For this case, HMAC-SHA1 or HMAC-256).
concatenatedSecrets = consumerSecret + "&" + tokenSecret;

byte[] bytesignature = calculateRFC2104HMACByte(signature, concatenatedSecrets);

// Encode the value result from prev using Base64.
signature = Base64.getEncoder().encodeToString(bytesignature);

return signature;
}

private static byte[] calculateRFC2104HMACByte(String data, String key)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
return mac.doFinal(data.getBytes());
}

}