Earlier this year, NetSuite released their new REST API called SuiteTalk REST Web Services. While still incomplete, this API provides a much friendlier interface for developers than its predecessors— the SOAP API and RESTlets. We chose to use the new REST API, because we believe it is the future for building integrations to NetSuite. The REST standard is much more intuitive, with many resources across many languages and frameworks.
Options for Authentication
The REST API offers two types of authentication: Token-Based Authentication (TBA) and OAuth 2.0. We chose to use TBA for the REST client.
Simply put, TBA is based on OAuth 1.0. Ultimately, you need to generate a request header that includes a signature created using tokens retrieved from the NetSuite dashboard and the OAuth 1.0 standard. Unfortunately, at the time of development, we hit a few walls trying to authenticate:
- The NetSuite documentation was only for their SOAP API and RESTlets
- There were no examples in Ruby
- Code samples relied on libraries or packages, making it hard to understand what was actually happening under the hood.
- The nonce and timestamp were not defined in the inputs of examples we found, so outputs would naturally vary given that these values are meant to change in practice.
- There were very few examples that had both inputs and signature output to test against.
In the end, the challenge of authenticating to NetSuite in the REST API was computing the signature. No examples we found worked for us, and regardless, we needed it in Ruby. Frustratingly, the only way to know if we were correct was to try API calls after making tweaks to our signature generation algorithm.
If you’re reading this post, hopefully this will save you a headache or two. We will clearly lay out our solution, break it down step-by-step, and we will give you some real examples. Let’s get going!
Our Solution
Let’s start with the full solution, which can be found in the LedgerSync library class LedgerSync::Ledgers::NetSuite::Token
(code can be found here). The Token handles creating the authorization header that will be used to sign the request. The request header looks something like this:
Authorization: OAuth realm="TEST_REALM",oauth_consumer_key="ef40afdd8abaac111b13825dd5e5e2ddddb44f86d5a0dd6dcf38c20aae6b67e4",oauth_token="2b0ce516420110bcbd36b69e99196d1b7f6de3c6234c5afb799b73d87569f5cc",oauth_signature_method="HMAC-SHA256",oauth_timestamp="1508242306",oauth_nonce="fjaLirsIcCGVZWzBX0pg",oauth_version="1.0",oauth_signature="i7MEtGwhCTIZbTsTrNGw9LdcERn4wsjt5C7TxmKWIfU%3D"
The Inputs
To compute a signature, we need a few things first. Let’s walk through each one and how to find it:
method
The request method will be one of the following:
POST
PUT
PATCH
GET
DELETE
consumer_key
, consumer_secret
The consumer key and secret can be retrieved from NetSuite by creating an Integration Record. Note that these values are only shown once at the end of creating a new Integration Record. Once you navigate away, you will no longer be able to see these values. You can reset the key and secret on existing Integration Records should you need to generate a new pair.
signature_method
NetSuite supports multiple signature methods. Our library uses HMAC-SHA256.
timestamp
We need to include a current timestamp in the signature and header.
nonce
We need to include a random alphanumeric string to be used in the signature and header.
oauth_version
The OAuth Version is defaults to “1.0”
realm
The realm is the NetSuite account ID. You can find this in your account or in the URL:
https://<ACCOUNT_ID>.app.netsuite.com/app/center/card.nl
If you are using a sandbox or test drive account, your account ID will include a hyphen and some other characters. For example, it may look like this: 9876543-sb1
.
Once you have the account ID, we will need to transform it to the format the API expects. You replace any hyphens with underscores (a.k.a. _
) and capitalize all letters. So 9876543-sb1
will become 9876543_SB1
.
token_id
, token_secret
These values can be found in NetSuite when you create an Access Token. Like the Integration Record, these values are only visible at the end of creating the token and will not be shown again. You can also reset these values on existing Access Tokens.
url
This is the URL of the request, which must include any query string parameters you intend to pass.
Example Values
For this guide, we will use the following values:
Variable | Value |
---|---|
method |
"GET" |
consumer_key |
"CONSUMER_KEY_VALUE" |
consumer_secret |
"CONSUMER_SECRET_VALUE" |
signature_method |
default, "HMAC-SHA256" |
timestamp |
Typically, you will leave this empty, but we will use 1234567890 |
nonce |
Typically, you will leave this empty, but we will use "asdfasdf" |
oauth_version |
default, "1.0" |
realm |
"9876543_SB1" |
token_id |
"TOKEN_ID_VALUE" |
token_secret |
"TOKEN_SECRET_VALUE" |
url |
"https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123?expandSubResources=true" |
nonce
and timestamp
should be left nil
or unpassed in practice. They are included here for purposes of having a consistent input and output, where typically they would be generated on the fly.
Throughout this tutorial, examples will appear in the following style. You can copy and paste this code in sequence to get the same results.
method = 'GET'
consumer_key = 'CONSUMER_KEY_VALUE'
consumer_secret = 'CONSUMER_SECRET_VALUE'
nonce = 'asdfasdf'
oauth_version = '1.0'
realm = '9876543_SB1'
signature_method = 'HMAC-SHA256'
timestamp = 1_234_567_890
token_id = 'TOKEN_ID_VALUE'
token_secret = 'TOKEN_SECRET_VALUE'
url = 'https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123?expandSubResources=true'
Generating the header
Now that we have our inputs, we can walk through computing the signature and header.
Before we dive in, please note that many values need to be escaped. If you see escape(...)
being used below, it is shorthand for the following:
def escape(str)
CGI.escape(str.to_s).gsub(/\+/, '%20')
end
Now let’s walk through each step:
1. Build data string
We need to create a string that will be used to compute a digest (a.k.a. signature) from. Using our values above, we can retrieve the string using the following:
token = LedgerSync::Ledgers::NetSuite::Token.new(
method: method,
consumer_key: consumer_key,
consumer_secret: consumer_secret,
realm: realm,
token_id: token_id,
token_secret: token_secret,
url: url
)
puts token.signature_data_string
"GET&https%3A%2F%2F9876543-sb1.suitetalk.api.netsuite.com%2Fservices%2Frest%2Frecord%2Fv1%2Fcustomer%2F123&expandSubResources%3Dtrue%26oauth_consumer_key%3DCONSUMER_KEY_VALUE%26oauth_nonce%3Dasdfasdf%26oauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3D1234567890%26oauth_token%3DTOKEN_ID_VALUE%26oauth_version%3D1.0"
But how is this created? It is composed of three values that are escaped and joined with &
. The values are as follows:
method
: Described in the inputs above.url_without_params
: Theurl
with all query parameters removed. In our example, this would behttps://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123
parameters_string
: A string representation of all URL parameters as well as the oauth parameters.
Let’s dive a level deeper and look into creating the parameters
string.
1a. Build parameters string
The parameters string is a sorted list of key/value pairs each joined with an ampersand (&
). The key/value pairs come from two sources:
- Query parameters parsed out of the URL, in our case
{ "expandSubResources" => "true }
- OAuth parameters
For our example, we would have the following:
url_params = {
"expandSubResources" => "true"
}
oauth_parameters_array = {
oauth_consumer_key: consumer_key,
oauth_nonce: nonce,
oauth_signature_method: signature_method,
oauth_timestamp: timestamp,
oauth_token: token_id,
oauth_version: oauth_version
}.to_a
parameters_string = url_params.to_a
.concat(oauth_parameters_array)
.map { |k, v| [escape(k), escape(v)] }
.sort { |a, b| a <=> b }
.map { |e| "#{e[0]}=#{e[1]}" }
.join('&')
puts parameters_string
"expandSubResources=true&oauth_consumer_key=CONSUMER_KEY_VALUE&oauth_nonce=asdfasdf&oauth_signature_method=HMAC-SHA256&oauth_timestamp=1234567890&oauth_token=TOKEN_ID_VALUE&oauth_version=1.0"
1b. Put it together
Now that we have our parameters_string
, we can generate the following:
url_without_params = "https://9876543-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123"
signature_data_string = [
method,
escape(url_without_params),
escape(parameters_string)
].join('&')
"GET&https%3A%2F%2F9876543-sb1.suitetalk.api.netsuite.com%2Fservices%2Frest%2Frecord%2Fv1%2Fcustomer%2F123&expandSubResources%3Dtrue%26oauth_consumer_key%3DCONSUMER_KEY_VALUE%26oauth_nonce%3Dasdfasdf%26oauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3D1234567890%26oauth_token%3DTOKEN_ID_VALUE%26oauth_version%3D1.0"
2. Build signature key
The final piece needed to generate a signature is the key. Our key is made by joining our consumer_secret
to token_secret
with an ampersand (&
):
key ||= [
consumer_secret,
token_secret
].join('&')
"CONSUMER_SECRET_VALUE&TOKEN_SECRET_VALUE"
3. Compute signature
Now with our signature_data_string
, we will use the desired signature_method
to compute a digest. We will assume we are using HMAC-SHA256
.
signature ||= Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest.new('sha256'),
key,
signature_data_string
)
).strip
"cId0B3hP0sFVQw/gjQ/P6YiOSx76u0WfyO8umOlq3gg="
4. Generate Header
Last but not least, we can now generate our header. The header is made by combining all of the inputs and our signature together in comma-separated, key-value pairs:
authorization_parts = [
[:realm, realm],
[:oauth_consumer_key, escape(consumer_key)],
[:oauth_token, escape(token_id)],
[:oauth_signature_method, signature_method],
[:oauth_timestamp, timestamp],
[:oauth_nonce, escape(nonce)],
[:oauth_version, oauth_version],
[:oauth_signature, escape(signature)]
]
headers = {
'Authorization' => "OAuth #{authorization_parts.map { |k, v| "#{k}=\"#{v}\"" }.join(',')}"
}
puts headers
{"Authorization"=>"OAuth realm=\"9876543_SB1\",oauth_consumer_key=\"CONSUMER_KEY_VALUE\",oauth_token=\"TOKEN_ID_VALUE\",oauth_signature_method=\"HMAC-SHA256\",oauth_timestamp=\"1234567890\",oauth_nonce=\"asdfasdf\",oauth_version=\"1.0\",oauth_signature=\"cId0B3hP0sFVQw%2FgjQ%2FP6YiOSx76u0WfyO8umOlq3gg%3D\""}
Wrapping up
All that is left is to use this header in a request. You will need a new signature (and therefore new header) per-request. And that’s it!
While not difficult in practice, it was by trial-and-error we were ultimately able to authenticate to NetSuite. Hopefully this post saves you some time!
It is highly recommended that you write comprehensive unit tests for this code. We wrote some tests in RSpec. These test are a great resource for examples you can test against.