6

I've tried implementing Netsuite's OAuth Example, as illustrated here: https://netsuite.custhelp.com/app/answers/detail/a_id/42165. I've posted it directly below so you don't have to go to the page if you don't want.

Unfortunately, it's not working. I know that I have the correct token and consumer key's and secrets, and the correct account ID. It's giving me a nice error though:

{"error" : {"code" : "INVALID_LOGIN_ATTEMPT", "message" : "Invalid login attempt."}}

I can look in my login audits, and see that it's saying that the signature is invalid. But the code itself looks fine and was provided by Netsuite.

I've also tried some approaches in Node.JS and haven't gotten them working. Any suggestions as to which direction I should go next?

import oauth2 as oauth
import requests
import time

url = "https://rest.netsuite.com/app/site/hosting/restlet.nl?script=992&deploy=1"
token = oauth.Token(key="080eefeb395df81902e18305540a97b5b3524b251772adf769f06e6f0d9dfde5", secret="451f28d17127a3dd427898c6b75546d30b5bd8c8d7e73e23028c497221196ae2")
consumer = oauth.Consumer(key="504ee7703e1871f22180441563ad9f01f3f18d67ecda580b0fae764ed7c4fd38", secret="b36d202caf62f889fbd8c306e633a5a1105c3767ba8fc15f2c8246c5f11e500c")

http_method = "GET"  
realm="ACCT123456"

params = {
    'oauth_version': "1.0",
    'oauth_nonce': oauth.generate_nonce(),
    'oauth_timestamp': str(int(time.time())),
    'oauth_token': token.key,
    'oauth_consumer_key': consumer.key
}

req = oauth.Request(method=http_method, url=url, parameters=params)
signature_method = oauth.SignatureMethod_HMAC_SHA1()
req.sign_request(signature_method, consumer, token)
header = req.to_header(realm)
headery = header['Authorization'].encode('ascii', 'ignore')
headerx = {"Authorization": headery, "Content-Type":"application/json"}
print(headerx)
conn = requests.get("https://rest.netsuite.com/app/site/hosting/restlet.nl?script=992&deploy=1",headers=headerx)
print(conn.text)

I've also implemented a few Node.JS samples (that haven't worked either). Here is one of them (CryptoJS HMAC-SHA1 and HMAC-SHA256 are on top, then the oauth-1.0a library, and then the code provided by Netsuite with a few small changes to make it work (added a hash_function, renamed 'public' to 'key'):

/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(g,l){var e={},d=e.lib={},m=function(){},k=d.Base={extend:function(a){m.prototype=this;var c=new m;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
p=d.WordArray=k.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=l?c:4*a.length},toString:function(a){return(a||n).stringify(this)},concat:function(a){var c=this.words,q=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var b=0;b<a;b++)c[f+b>>>2]|=(q[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535<q.length)for(b=0;b<a;b+=4)c[f+b>>>2]=q[b>>>2];else c.push.apply(c,q);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=k.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b<a;b+=4)c.push(4294967296*g.random()|0);return new p.init(c,a)}}),b=e.enc={},n=b.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f<a;f++){var d=c[f>>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f<c;f+=2)b[f>>>3]|=parseInt(a.substr(f,
2),16)<<24-4*(f%8);return new p.init(b,c/2)}},j=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f<a;f++)b.push(String.fromCharCode(c[f>>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f<c;f++)b[f>>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new p.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(j.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return j.parse(unescape(encodeURIComponent(a)))}},
r=d.BufferedBlockAlgorithm=k.extend({reset:function(){this._data=new p.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,b=c.words,f=c.sigBytes,d=this.blockSize,e=f/(4*d),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*d;f=g.min(4*a,f);if(a){for(var k=0;k<a;k+=d)this._doProcessBlock(b,k);k=b.splice(0,a);c.sigBytes-=f}return new p.init(k,f)},clone:function(){var a=k.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});d.Hasher=r.extend({cfg:k.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){r.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new s.HMAC.init(a,
d)).finalize(b)}}});var s=e.algo={};return e}(Math);
(function(){var g=CryptoJS,l=g.lib,e=l.WordArray,d=l.Hasher,m=[],l=g.algo.SHA1=d.extend({_doReset:function(){this._hash=new e.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(d,e){for(var b=this._hash.words,n=b[0],j=b[1],h=b[2],g=b[3],l=b[4],a=0;80>a;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(n<<5|n>>>27)+l+m[a];c=20>a?c+((j&h|~j&g)+1518500249):40>a?c+((j^h^g)+1859775393):60>a?c+((j&h|j&g|h&g)-1894007588):c+((j^h^
g)-899497514);l=g;g=h;h=j<<30|j>>>2;j=n;n=c}b[0]=b[0]+n|0;b[1]=b[1]+j|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+l|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,g=8*d.sigBytes;e[g>>>5]|=128<<24-g%32;e[(g+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(g+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(l);g.HmacSHA1=d._createHmacHelper(l)})();
(function(){var g=CryptoJS,l=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=l.parse(d));var g=e.blockSize,k=4*g;d.sigBytes>k&&(d=e.finalize(d));d.clamp();for(var p=this._oKey=d.clone(),b=this._iKey=d.clone(),n=p.words,j=b.words,h=0;h<g;h++)n[h]^=1549556828,j[h]^=909522486;p.sigBytes=b.sigBytes=k;this.reset()},reset:function(){var e=this._hasher;e.reset();e.update(this._iKey)},update:function(e){this._hasher.update(e);return this},finalize:function(e){var d=
this._hasher;e=d.finalize(e);d.reset();return d.finalize(this._oKey.clone().concat(e))}})})();

/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(h,s){var f={},g=f.lib={},q=function(){},m=g.Base={extend:function(a){q.prototype=this;var c=new q;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},
r=g.WordArray=m.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=s?c:4*a.length},toString:function(a){return(a||k).stringify(this)},concat:function(a){var c=this.words,d=a.words,b=this.sigBytes;a=a.sigBytes;this.clamp();if(b%4)for(var e=0;e<a;e++)c[b+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)c[b+e>>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<<
32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d<a;d+=4)c.push(4294967296*h.random()|0);return new r.init(c,a)}}),l=f.enc={},k=l.Hex={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++){var e=c[b>>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b+=2)d[b>>>3]|=parseInt(a.substr(b,
2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b<a;b++)d.push(String.fromCharCode(c[b>>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b<c;b++)d[b>>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}},
u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);c.sigBytes-=b}return new r.init(g,b)},clone:function(){var a=m.clone.call(this);
a._data=this._data.clone();return a},_minBufferSize:0});g.Hasher=u.extend({cfg:m.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){u.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(c,d){return(new a.init(d)).finalize(c)}},_createHmacHelper:function(a){return function(c,d){return(new t.HMAC.init(a,
d)).finalize(c)}}});var t=f.algo={};return f}(Math);
(function(h){for(var s=CryptoJS,f=s.lib,g=f.WordArray,q=f.Hasher,f=s.algo,m=[],r=[],l=function(a){return 4294967296*(a-(a|0))|0},k=2,n=0;64>n;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]=
c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes;
d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math);
(function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j<h;j++)k[j]^=1549556828,n[j]^=909522486;r.sigBytes=l.sigBytes=m;this.reset()},reset:function(){var f=this._hasher;f.reset();f.update(this._iKey)},update:function(f){this._hasher.update(f);return this},finalize:function(f){var g=
this._hasher;f=g.finalize(f);g.reset();return g.finalize(this._oKey.clone().concat(f))}})})();

//oauth-1.0a

if (typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') {
    module.exports = OAuth;
}

/**
 * Constructor
 * @param {Object} opts consumer key and secret
 */
function OAuth(opts) {
    if(!(this instanceof OAuth)) {
        return new OAuth(opts);
    }

    if(!opts) {
        opts = {};
    }

    if(!opts.consumer) {
        throw new Error('consumer option is required');
    }

    this.consumer            = opts.consumer;
    this.nonce_length        = opts.nonce_length || 32;
    this.version             = opts.version || '1.0';
    this.parameter_seperator = opts.parameter_seperator || ', ';
    this.realm               = opts.realm;

    if(typeof opts.last_ampersand === 'undefined') {
        this.last_ampersand = true;
    } else {
        this.last_ampersand = opts.last_ampersand;
    }

    // default signature_method is 'PLAINTEXT'
    this.signature_method = opts.signature_method || 'PLAINTEXT';

    if(this.signature_method == 'PLAINTEXT' && !opts.hash_function) {
        opts.hash_function = function(base_string, key) {
            return key;
        }
    }

    if(!opts.hash_function) {
        throw new Error('hash_function option is required');
    }

    this.hash_function = opts.hash_function;
    this.body_hash_function = opts.body_hash_function || this.hash_function;
}

/**
 * OAuth request authorize
 * @param  {Object} request data
 * {
 *     method,
 *     url,
 *     data
 * }
 * @param  {Object} key and secret token
 * @return {Object} OAuth Authorized data
 */
OAuth.prototype.authorize = function(request, token) {
    var oauth_data = {
        oauth_consumer_key: this.consumer.key,
        oauth_nonce: this.getNonce(),
        oauth_signature_method: this.signature_method,
        oauth_timestamp: this.getTimeStamp(),
        oauth_version: this.version
    };

    if(!token) {
        token = {};
    }

    if(token.key !== undefined) {
        oauth_data.oauth_token = token.key;
    }

    if(!request.data) {
        request.data = {};
    }

    if(request.includeBodyHash) {
    oauth_data.oauth_body_hash = this.getBodyHash(request, token.secret)
    }

    oauth_data.oauth_signature = this.getSignature(request, token.secret, oauth_data);

    return oauth_data;
};

/**
 * Create a OAuth Signature
 * @param  {Object} request data
 * @param  {Object} token_secret key and secret token
 * @param  {Object} oauth_data   OAuth data
 * @return {String} Signature
 */
OAuth.prototype.getSignature = function(request, token_secret, oauth_data) {
    return this.hash_function(this.getBaseString(request, oauth_data), this.getSigningKey(token_secret));
};

/**
 * Create a OAuth Body Hash
 * @param {Object} request data
 */
OAuth.prototype.getBodyHash = function(request, token_secret) {
var body = typeof request.data === 'string' ? request.data : JSON.stringify(request.data)

if (!this.body_hash_function) {
    throw new Error('body_hash_function option is required');
}

return this.body_hash_function(body, this.getSigningKey(token_secret))
};

/**
 * Base String = Method + Base Url + ParameterString
 * @param  {Object} request data
 * @param  {Object} OAuth data
 * @return {String} Base String
 */
OAuth.prototype.getBaseString = function(request, oauth_data) {
    return request.method.toUpperCase() + '&' + this.percentEncode(this.getBaseUrl(request.url)) + '&' + this.percentEncode(this.getParameterString(request, oauth_data));
};

/**
 * Get data from url
 * -> merge with oauth data
 * -> percent encode key & value
 * -> sort
 *
 * @param  {Object} request data
 * @param  {Object} OAuth data
 * @return {Object} Parameter string data
 */
OAuth.prototype.getParameterString = function(request, oauth_data) {
    var base_string_data;
    if (oauth_data.oauth_body_hash) {
        base_string_data = this.sortObject(this.percentEncodeData(this.mergeObject(oauth_data, this.deParamUrl(request.url))));
    } else {
        base_string_data = this.sortObject(this.percentEncodeData(this.mergeObject(oauth_data, this.mergeObject(request.data, this.deParamUrl(request.url)))));
    }

    var data_str = '';

    //base_string_data to string
    for(var i = 0; i < base_string_data.length; i++) {
        var key = base_string_data[i].key;
        var value = base_string_data[i].value;
        // check if the value is an array
        // this means that this key has multiple values
        if (value && Array.isArray(value)){
        // sort the array first
        value.sort();

        var valString = "";
        // serialize all values for this key: e.g. formkey=formvalue1&formkey=formvalue2
        value.forEach((function(item, i){
            valString += key + '=' + item;
            if (i < value.length){
            valString += "&";
            }
        }).bind(this));
        data_str += valString;
        } else {
        data_str += key + '=' + value + '&';
        }
    }

    //remove the last character
    data_str = data_str.substr(0, data_str.length - 1);
    return data_str;
};

/**
 * Create a Signing Key
 * @param  {String} token_secret Secret Token
 * @return {String} Signing Key
 */
OAuth.prototype.getSigningKey = function(token_secret) {
    token_secret = token_secret || '';

    if(!this.last_ampersand && !token_secret) {
        return this.percentEncode(this.consumer.secret);
    }

    return this.percentEncode(this.consumer.secret) + '&' + this.percentEncode(token_secret);
};

/**
 * Get base url
 * @param  {String} url
 * @return {String}
 */
OAuth.prototype.getBaseUrl = function(url) {
    return url.split('?')[0];
};

/**
 * Get data from String
 * @param  {String} string
 * @return {Object}
 */
OAuth.prototype.deParam = function(string) {
    var arr = string.split('&');
    var data = {};

    for(var i = 0; i < arr.length; i++) {
        var item = arr[i].split('=');

        // '' value
        item[1] = item[1] || '';

        // check if the key already exists
        // this can occur if the QS part of the url contains duplicate keys like this: ?formkey=formvalue1&formkey=formvalue2
        if (data[item[0]]){
        // the key exists already
        if (!Array.isArray(data[item[0]])) {
            // replace the value with an array containing the already present value
            data[item[0]] = [data[item[0]]];
        }
        // and add the new found value to it
        data[item[0]].push(decodeURIComponent(item[1]));
        } else {
        // it doesn't exist, just put the found value in the data object
        data[item[0]] = decodeURIComponent(item[1]);
        }
    }

    return data;
};

/**
 * Get data from url
 * @param  {String} url
 * @return {Object}
 */
OAuth.prototype.deParamUrl = function(url) {
    var tmp = url.split('?');

    if (tmp.length === 1)
        return {};

    return this.deParam(tmp[1]);
};

/**
 * Percent Encode
 * @param  {String} str
 * @return {String} percent encoded string
 */
OAuth.prototype.percentEncode = function(str) {
    return encodeURIComponent(str)
        .replace(/\!/g, "%21")
        .replace(/\*/g, "%2A")
        .replace(/\'/g, "%27")
        .replace(/\(/g, "%28")
        .replace(/\)/g, "%29");
};

/**
 * Percent Encode Object
 * @param  {Object} data
 * @return {Object} percent encoded data
 */
OAuth.prototype.percentEncodeData = function(data) {
    var result = {};

    for(var key in data) {
        var value = data[key];
        // check if the value is an array
        if (value && Array.isArray(value)){
        var newValue = [];
        // percentEncode every value
        value.forEach((function(val){
            newValue.push(this.percentEncode(val));
        }).bind(this));
        value = newValue;
        } else {
        value = this.percentEncode(value);
        }
        result[this.percentEncode(key)] = value;
    }

    return result;
};

/**
 * Get OAuth data as Header
 * @param  {Object} oauth_data
 * @return {String} Header data key - value
 */
OAuth.prototype.toHeader = function(oauth_data) {
    var sorted = this.sortObject(oauth_data);

    var header_value = 'OAuth ';

    if (this.realm) {
        header_value += 'realm="' + this.realm + '"' + this.parameter_seperator;
    }

    for(var i = 0; i < sorted.length; i++) {
        if (sorted[i].key.indexOf('oauth_') !== 0)
            continue;

        header_value += this.percentEncode(sorted[i].key) + '="' + this.percentEncode(sorted[i].value) + '"' + this.parameter_seperator;
    }

    return {
        Authorization: header_value.substr(0, header_value.length - this.parameter_seperator.length) //cut the last chars
    };
};

/**
 * Create a random word characters string with input length
 * @return {String} a random word characters string
 */
OAuth.prototype.getNonce = function() {
    var word_characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    var result = '';

    for(var i = 0; i < this.nonce_length; i++) {
        result += word_characters[parseInt(Math.random() * word_characters.length, 10)];
    }

    return result;
};

/**
 * Get Current Unix TimeStamp
 * @return {Int} current unix timestamp
 */
OAuth.prototype.getTimeStamp = function() {
    return parseInt(new Date().getTime()/1000, 10);
};

////////////////////// HELPER FUNCTIONS //////////////////////

/**
 * Merge object
 * @param  {Object} obj1
 * @param  {Object} obj2
 * @return {Object}
 */
OAuth.prototype.mergeObject = function(obj1, obj2) {
    obj1 = obj1 || {};
    obj2 = obj2 || {};

    var merged_obj = obj1;
    for(var key in obj2) {
        merged_obj[key] = obj2[key];
    }
    return merged_obj;
};

/**
 * Sort object by key
 * @param  {Object} data
 * @return {Array} sorted array
 */
OAuth.prototype.sortObject = function(data) {
    var keys = Object.keys(data);
    var result = [];

    keys.sort();

    for(var i = 0; i < keys.length; i++) {
        var key = keys[i];
        result.push({
            key: key,
            value: data[key],
        });
    }

    return result;
};

//NETSUITE'S RESTLET

function callRESTlet(request, response) {
    var remoteAccountID = 'ACCOUNT ID HERE';
    var restletUrl = 'https://rest.netsuite.com/app/site/hosting/restlet.nl?script=992&deploy=1';
    //user token
    var token = {
        key: 'ACCESS KEY HERE',
        secret: 'ACCESS SECRET HERE'
    };
    //app credentials
    var oauth = OAuth({
        consumer: {
            key: 'INTEGRATION KEY HERE',
            secret: 'INTEGRATION SECRET HERE'
        },
        signature_method: 'HMAC-SHA1',
        hash_function: function(base_string, key)
        {
            return CryptoJS.HmacSHA1(base_string, key).toString(CryptoJS.enc.Base64);
        }
    });

    var request_data = {
        url: restletUrl,
        method: 'GET',
        data: {}
    };

    var oauth_data = {
        oauth_consumer_key: oauth.consumer.key,
        oauth_nonce: oauth.getNonce(),
        oauth_signature_method: oauth.signature_method,
        oauth_timestamp: oauth.getTimeStamp(),
        oauth_version: '1.0',
        oauth_token: token.key,
        realm: remoteAccountID
    };

    var headerWithRealm = oauth.toHeader(oauth.authorize(request_data, token));
    headerWithRealm.Authorization += ',realm="' + remoteAccountID + '"';

    var restResponse = nlapiRequestURL(restletUrl, null, headerWithRealm, null, "GET");

    var html = 'Calling: ' +
        restletUrl +
        '<br><br>' +
        'Generated OAuth header:<br>' +
        headerWithRealm.Authorization +
        '<br><br>' +
        'Response:<br>' +
        restResponse.getBody() 
    response.write(html);
}
M. Pope
  • 411
  • 6
  • 16

3 Answers3

6

EDIT: Just published an npm module which should make things easier: https://www.npmjs.com/package/nsrestlet


Was able to get some code working after hunting through GitHub Code commits. Still, bknights response is really good.

Here's what I got working.

Assuming you have Node.js and npm installed, run:

npm install request
npm install oauth-1.0a@1.0.1

It's really important that it's version 1.0.1.

Once you have that, this code should work:

/*
    =================    REQUIRED USER ACCOUNT INFORMATION    ==============================================
*/

var accountID = 'PUT ACCOUNT ID HERE';

var token = {
    public: 'PUT TOKEN KEY HERE',
    secret: 'PUB TOKEN SECRET HERE'
};

var consumer = {
    public: 'PUT CONSUMER KEY HERE',
    secret: 'PUT CONSUMER SECRET HERE'
};

//use the full restlet URL, not the rest.netsuite.com URL
//for example, https://YOURACCOUNTNUMBER.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=SCRIPTNUMBER&deploy=DEPLOYNUMBER
var restlet_url = 'PUT YOUR RESTLET URL HERE';

/*
    =========================================================================================================
*/

//REQUIRED NPM MODULES
const request = require('request');
const OAuth   = require('oauth-1.0a');      //version 1.0.1, don't do version 1.1.0

//SET UP THE OAUTH OBJECT
var oauth = OAuth({
    consumer: consumer,
    signature_method: 'HMAC-SHA256'         //you can also use HMAC-SHA1 but HMAC-SHA256 is more secure (supposedly)
});

//SET UP THE REQUEST OBJECT
var request_data = {
    url: restlet_url,
    method: 'POST',
};

//GET THE AUTHORIZATION AND STICK IT IN THE HEADER, ALONG WITH THE REALM AND CONTENT-TYPE
var authorization = oauth.authorize(request_data, token);
var header = oauth.toHeader(authorization);
header.Authorization += ', realm="' + accountID + '"';
header['content-type'] = 'application/json';

//MAKE THE REQUEST
request({
    url: request_data.url,
    method: request_data.method,
    headers: header,
    json: {
        message: "test123"                  //this is your payload
    }
}, function(error, response, body) {
    if(error)
    {
        console.log(error);
    }
    else
    {
        console.log(body);
    }
});

If anybody has any problems with this code, leave a response and I'll do my best to help.

M. Pope
  • 411
  • 6
  • 16
  • I'm trying this one, and (as with every other option I try - other than POSTman) it just returns a 403 - Invalid login attempt. In NetSuite is shows as Invalid Signature (same error I get any other way I try it. Any thoughts? – w3bguy Sep 11 '18 at 15:52
  • 1
    Hey W3BGUY, what verison of Node are you running? And what Operating System? Did you install the npm package, or did you use the code posted here directly? – M. Pope Sep 11 '18 at 17:12
  • 1
    Also @W3BGUY did you replace the account number in the url listed above? Sorry just want to check the basics (which you likely already have fine) before looking at more complex stuff. – M. Pope Sep 11 '18 at 23:00
  • Hey M. Pope. Thanks for the response. I have node 10.10.0 installed right now. I'm using a macbook pro (work computer). I did install npm, and the nsrestlet package. It all appears to run properly, but I've yet to get a valid login connection via anything but Postman. I did use the code above, as well, to try and get started. I did update all of the variables :). Any other thoughts? – w3bguy Sep 12 '18 at 14:34
  • Hey M. Pope. Thanks again. I did get yours working (I had to add in the . So that helped a lot. I had to add in hash_function() to the OAuth initialization. Do you have any docs on how you set that up? My use case is that I need to build this out into another application (using vanilla JS), so I can't use the node packages. This did help create the PoC, though, so that's awesome. :) – w3bguy Sep 14 '18 at 16:58
  • 1
    Hey @W3BGUY, sorry about not getting back to you until now - I have a bad cold, so getting better right now. Where were you missing the period, just curious, because perhaps I need to make the documentation better. And which of my code bits did you use where you had to add the hash function, just curious? The node module should already have it so I'm worried if it isn't working. If you want to get vanilla JS, the easiest way might be to export it to a file using browserify, but I've never actually used it before. (continued in next post) – M. Pope Sep 15 '18 at 19:57
  • 1
    Crypto JS is what node's require('crypto') was before it was integrated into the node ecosystem (it used to be it's own library) so you should be able to use the copy above with the two functions I included. Oauth 1.0a should work in both the browser and node, so you could use that directly... Here's an example of one that I believe is used in a Suitescript 2.0 Suitelet... https://github.com/mark-keaton/oauth-netsuite. But yeah, let me know what works for you and I can edit things in so things are better and clearer. – M. Pope Sep 15 '18 at 20:02
  • Hey, M. Pope. Hope you are feeling better. :) I did not have to add period anywhere, I only had to add the hash_function() to the OAuth object. That was from using the code in this post. I'll take a look at browserify, thanks for that suggestion. I have been able to use the Crypto JS code to build an authentication for interacting with the NetSuite WSDL, in out app. Just not for creating the actual http auth header. I did try that oauth-netsuite repo, but that is for working in NetSuite. I'm trying to work outside NetSuite, calling in to a RESTlet. Thanks again! :) – w3bguy Sep 17 '18 at 12:07
  • 2
    Hey W3BGuy, oh yeah, the hash_function(). That's because of the version of the oauth library you are using. The earlier versions did the hash_function for you, if I remember right (it's been a month or two) whereas the later versions required you to provide your own. I remember it being a bit of a pain point. I tried looking around for some other samples... but couldn't find any. I'll do some more searching later. – M. Pope Sep 20 '18 at 15:40
4

Netsuite's node samples use oauth-1.0a

https://netsuite.custhelp.com/app/answers/detail/a_id/42171

and their sample from https://netsuite.custhelp.com/app/answers/detail/a_id/42172/

is what I've had in production for a couple of years and works well.

var Promise = require('bluebird');
var request = require('request');
var crypto = require('crypto');
var OAuth = require('oauth-1.0a');
var Agent = require('https').Agent;
//var debug = require('debug')('kotn-ns');


function promiseTry(pSrc, maxTries, minDelay, maxDelay, canRetry){ //NS prone to spurious failures due to overloading
    return new Promise(function(resolve, reject){

        minDelay = minDelay || 0;
        var delaySize = maxDelay - minDelay;
        var t = function(){ return Math.floor(Math.random()* delaySize)+ minDelay;};
        var firstReason = null;

        function doRetry(triesLeft){

            pSrc().then(function(data){
                resolve(data);
            }).catch(function(reason){
                if(!firstReason) firstReason = reason;
                console.error('in retry error with '+reason.toString());
                if(triesLeft  && canRetry(reason)) setTimeout(function(){ doRetry(triesLeft-1);}, t());
                else reject(firstReason);
            });
        }
        doRetry(maxTries -1);
    });
}

function hasReason(msg, reasons){
    for(var i = 0; i< reasons.length;i++){
        if(msg.indexOf(reasons[i]) != -1) return true;
    }
    return false;
}

var agentPool = {};
function getAgent(accountId, tokenId){
    var agentKey = accountId+'::'+ tokenId;
    var agent = agentPool[agentKey];
    if(!agent){
        console.log('new agent for '+agentKey)
        agent = new Agent({
            keepAlive:false,
            maxSockets:5
        });
        agentPool[agentKey] = agent;
    }
    return agent;
}


/**
 * [RESTHandler description]
 * @param {options} options {accountId, consumerKey,consumerSecret,tokenId,tokenSecret}
 */
function RESTHandler(options) {



    var config = Object.assign({
        maxTries:3,
        minRetryDelay: 800,
        maxRetryDelay:30000,
        canRetry: function(reason){
            var reasonText =  reason.message || JSON.stringify(reason);
            if(hasReason(reasonText, ['ECONNRESET', 'ESOCKETTIMEDOUT','ETIMEDOUT', 'SSS_REQUEST_LIMIT_EXCEEDED'])) {
                console.error('retrying because: '+reasonText);
                return true;
            }
            console.error('no retry with: '+reasonText);
            return false;
        }
    }, options);

    var oauth = OAuth({
        consumer: {
            key: config.consumerKey,
            secret: config.consumerSecret
        },
        signature_method: 'HMAC-SHA1',
        parameter_seperator: ',',
        hash_function: function(base_string, key) {
            return crypto.createHmac('sha1', key).update(base_string).digest('base64');
        }
    });
    var token = {
        key: config.tokenId,
        secret: config.tokenSecret
    };

    function makeRequest(url, method, payload) {
        var requestData = {
            url: url,
            method: method
        };
        if(payload){
            requestData.body = payload;
        }

        var headers = oauth.toHeader(oauth.authorize(requestData, token));
        headers.Authorization += ',realm="' + config.accountId + '"';
        headers.authorization = headers.Authorization;
        delete headers.Authorization;
        headers['content-type'] = 'application/json';
        headers['accept'] = 'application/json';

        //console.log(JSON.stringify(headers, null, '  '));
        requestData.headers = headers;
        Object.assign(requestData, {
            pool:getAgent(config.accountId, config.tokenId),
            timeout : 30000,
            strictSSL : true
        });

        // requestData.json = true;

        // return new Promise(function(resolve){
        //     resolve({'headers' : 'done'});
        // });



        var processRequest = function(){
            return new Promise(function(resolve, reject) {
                request(requestData, function(error, response, body) {
                    if(error){
                        console.error('error calling: '+ requestData.url);
                        console.error(error);
                        reject((error instanceof Error) ? error : new Error(JSON.stringify(error)));
                        return;
                    }
                    if(!body || !(/"success"/).test(body)) {
                        console.log(method +' '+ response.statusCode +' '+ url +'\n\t'+body);
                        reject(new Error(body || 'unexpected error'));
                        return;
                    }
                    try{
                        resolve(JSON.parse(body));
                    }catch(e){
                        console.trace(e);
                        reject(e);
                    }
                });
            });
        };
        return promiseTry(processRequest, config.maxTries, config.maxRetryDelay, config.minRetryDelay, config.canRetry);
    }

    return{
        get: function(url){
            return makeRequest(url, 'GET');
        },
        put: function(url, data){
            return makeRequest(url, 'PUT', data);
        },
        post: function(url, data){
            return makeRequest(url, 'POST', data);
        },
        destroy : function(){
            //nsAgent.destroy();
        }
    };
}
module.exports = RESTHandler;
bknights
  • 14,408
  • 2
  • 18
  • 31
  • Hey, I just implemented that example... and I haven't been able to get it working either. I put my code up above (since it's too large for comments). It's the two libraries mentioned in their sample, as well as their sample. Had to do two edits though to prevent the code from erroring( added a hash_function, and renamed consumer.public to consumer.key, and token.public to token.key). It still is giving me an Invalid Login Attempt Error though (for Invalid Signature). Still, this is helpful. – M. Pope May 31 '18 at 17:50
  • I've added code. If this doesn't work then there must be something else going on. TLS version and data center hostname have both shown up recently as issues for customers. – bknights May 31 '18 at 21:33
  • Hey bknights, thanks you for the code. It's really well written. I might start using Object.assign now thanks to you... =). Yeah, I'm still getting the error, but your code helped a lot. I might contact Netsuite on this one. – M. Pope May 31 '18 at 22:40
  • Hey Bknights, also, a question. I might release an NPM module to make this easier for everyone else in the future. I like the idea of the facade that you used with separate functions for get, put, post, and destroy. Do you mind if I do something similar? (Not using your code at all, just like the idea). If not, that's fine too. – M. Pope Jun 01 '18 at 23:31
  • Well my code is based on a bunch of open source and I posted it here publicly so just attribute it in your code and feel free to use it – bknights Jun 02 '18 at 02:47
  • The links are useless. You can't click on them because they're behind the worst login-wall of the planet. Once you login, you don't get redirected to the URL. Their session management is atrocious, so you can't login and then try to get to that URL again because, you guessed it, you'll be required to login again and then you aren't redirected. AGAIN. NetSuite-- – genio Oct 02 '18 at 15:50
  • Guys, I am using Restlet with C# with TBA Oauth 1.0 and OAuthBase.cs whenever I append a special character with query string its get messed and return as INVALID_LOGIN_ATTEMPT have a look at below string restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=198&deploy=1&columns=internalid,itemid&type=item&filters=matrix;is;F – MUHAMMAD MUBUSHER ASLAM Nov 11 '20 at 11:10
0

More up-to-date as of 2022-12-12 replaced request with its successor needle and removed the now unnecessary bluebird

const needle = require('needle');
const crypto = require('crypto');
const OAuth = require('oauth-1.0a');
const Agent = require('https').Agent;
const debug = require('debug')('kotn-nso');


function promiseTry(pSrc, maxTries, minDelay, maxDelay, canRetry){ //NS prone to spurious failures due to overloading
    return new Promise((resolve, reject)=>{

        minDelay = minDelay || 0;
        const delaySize = maxDelay - minDelay;
        const t = function(){ return Math.floor(Math.random()* delaySize)+ minDelay;};
        let firstReason = null;

        function doRetry(triesLeft){

            pSrc().then((data)=>{
                resolve(data);
            }).catch((reason)=>{
                if(!firstReason) firstReason = reason;
                console.error('in retry error with '+ triesLeft +' for '+reason.toString());
                if(triesLeft  && canRetry(reason)){
                    setTimeout(()=>{ doRetry(triesLeft-1);}, t());
                } 
                else reject(firstReason);
            });
        }
        doRetry(maxTries -1);
    });
}

function hasReason(msg, reasons){
    for(var i = 0; i< reasons.length;i++){
        if(msg.indexOf(reasons[i]) != -1) return true;
    }
    return false;
}

var agentPool = {};
function getAgent(accountId, maxSockets){
    var agentKey = accountId+'::'; // + tokenId;
    var agent = agentPool[agentKey];
    if(!agent){
        console.log('new agent for '+agentKey);
        agent = new Agent({
            keepAlive:false,
            maxSockets:maxSockets || 2 // one in reserve for slow closers
        });
        agentPool[agentKey] = agent;
    }
    return agent;
}

/**
 * [RESTHandler description]
 * @param {options} options {accountId, consumerKey,consumerSecret,tokenId,tokenSecret,maxSockets}
 */
function RESTHandler(options) {


    const config = Object.assign({
        maxTries:3,
        minRetryDelay: 400,
        maxRetryDelay:30000,
        canRetry: function(reason){
            var reasonText =  reason.message || JSON.stringify(reason);
            if(hasReason(reasonText, ['ECONNRESET', 'ESOCKETTIMEDOUT','ETIMEDOUT', 'SSS_REQUEST_LIMIT_EXCEEDED'])) {
                console.error('retrying because: '+reasonText);
                return true;
            }
            console.error('no retry with: '+reasonText);
            return false;
        }
    }, options);

    const oauth = OAuth({
        consumer: {
            key: config.consumerKey,
            secret: config.consumerSecret
        },
        signature_method: 'HMAC-SHA256',
        parameter_seperator: ',',
        hash_function: function(base_string, key) {
            return crypto.createHmac('sha256', key).update(base_string).digest('base64');
        }
    });
    const token = {
        key: config.tokenId,
        secret: config.tokenSecret
    };

    function makeRequest(url, method, payload) {
        debug(method +' '+ JSON.stringify(url));
        var requestData = {
            url: url,
            method: method
        };
        if(payload){
            requestData.body = payload;
        }
        
        var headers = oauth.toHeader(oauth.authorize(requestData, token));
        headers.Authorization += ',realm="' + config.accountId + '"';
        headers.authorization = headers.Authorization;
        delete headers.Authorization;
        headers['content-type'] = 'application/json';
        headers['accept'] = 'application/json';

        //console.log(JSON.stringify(headers, null, '  '));
        const options = {
            headers:headers,
            agent:getAgent(config.accountId, config.maxSockets), 
            timeout : 30000,
            strictSSL : true,
            time:false
        };
    
        var processRequest = function(){
            var headers = null;
            return needle(method.toLowerCase(), url, payload, options).then(resp=>{
                debug(resp.statusCode +' ' + JSON.stringify(resp.headers, null, ' '));
                headers = resp.headers;

                if(resp.statusCode != 200 && resp.statusCode != 201){
                    throw new Error(resp.statusCode +': '+ (resp.body || 'unexpected error'));
                }
                if(!resp.body) throw new Error('Unexpected Response');
                return resp.body;
            }).catch(error=>{
                if(headers){
                    debug('Error returned with ' + JSON.stringify(headers));
                }
                const msg = error instanceof Error ? error.message : JSON.stringify(error);
                console.error(method +' error on '+ url + ', '+ msg);
                throw (error instanceof Error) ? error : new Error(msg);
            });
        };
        return promiseTry(processRequest, config.maxTries, config.maxRetryDelay, config.minRetryDelay, config.canRetry);
    }

    return{
        get: function(url){
            return makeRequest(url, 'GET');
        },
        put: function(url, data){
            return makeRequest(url, 'PUT', data);
        },
        post: function(url, data){
            return makeRequest(url, 'POST', data);
        },
        destroy : function(){
            //nsAgent.destroy();
        }
    };
}
module.exports = RESTHandler;
bknights
  • 14,408
  • 2
  • 18
  • 31