3

I'm updating a NetSuite integration for DocuSign to convert all scripting to SuiteScript 2.0 and update the authentication to utilize the JWT grant method, seeing as the current method looks like it will be deprecated for new applications in a couple weeks, and completely some time next year. I do have a couple questions about it all, though, that I'm having trouble answering.

For starters, when setting the IAT and EXP values for the body, am I getting this value based on the current server time (that varies depending on which time zone the account's datacenter happens to reside), or should it be based on getting the UTC time instead? The documentation didn't seem clear enough to me on that matter.

Additionally, I am unclear how to go about creating the signature. Retrieving and preparing the necessary data, aside form the aforementioned issue, is not a problem. But, arranging the data to be encoded based on what I've seen available within the 2.0 modules as compared to examples I've seen for creating the signature don't seem to align so well. For example, this is an approximation of what I have seen:

var header = { typ : "JWT" , alg : "RS256" };
var body = { iss : "abc123" , sub : "def456" , iat : 123456789 , exp : 123456789 + (60 * 60) , scope : "signature" };
var encHeader = base64UrlEncode(JSON.stringify(header));
var encBody = base64UrlEncode(JSON.stringify(body));

// the key pairs are stored elsewhere, and the functions represent a way to retrieve the key contents
var publickKey = retrievePublicKey();
var privateKey = retrievePrivateKey();
var signature = RS256Encode(
    encHeader + "." + encBody ,
    publicKey ,
    privateKey
);

The examples show different encryption functions that all take in three arguments in same way demonstrated above. However, when looking at what is available within the native SuiteScript 2.0 modules, I don't see anything quite comparable. I see that the crypto module (which gets pulled into other modules it seems) does have the ability to perform the required RSA SHA256 encoding, but not quite sure what the deal is to make sure that all the pieces are taken in together correctly.

I've taken a look at the JavaScript libraries for JWT creation at https://jwt.io/, but I have no experience with attempting to incorporate node modules into a SuiteScript project, if it's even possible.

So, is there a way to natively construct a JWT within SuiteScript, or am I going to have to find a way to be able to reference a node module in a script?

-- EDIT --

Ok, so it looks like I can use "Asynchronous Module Definition" (AMD) to import modules into a script by creating a JSON config file and adding the following to a script file's JSDoc header:

*@NAmdConfig  /path/to/myModule.json

I figure that since I need it to be a relative path given that this project will be distributed to other accounts, something like this ought to work if the JSON config file is in the same directory:

*@NAmdConfig ./nodeModules.json

But I'm having a heck of a time determining exactly how to setup the JSON config. I can't quite seem to find anything that really helps determine how to build it correctly. The biggest issue is determining whether or not a module is AMD or non-AMD, and in the case of non-AMD whether to attempt to import the scripts under the CJS section or the ESM section.

For any additional reference, I used NPM to install the jose module since it's compatible with JavaScript/Node and has the encryption methods I need.

npm install jose

Right now, the path for it in the source project looks something like this:

SuiteScripts/Project_Name/lib/node_modules/jose

And the script file that will be referencing the module is located under this path:

SuiteScripts/Project_Name/lib
Michael McCauley
  • 853
  • 1
  • 12
  • 37

1 Answers1

6
/**
 *@NApiVersion 2.x
 */

 define(['N/encode', 'N/crypto/certificate'], function(encode, cert){

   function signIt(payload, ttl){


      if(typeof payload.exp == 'undefined'){
         var secondsSinceEpoch = Math.round(Date.now() / 1000);
         var expAt = secondsSinceEpoch + (ttl || 60); 
         payload["exp"] = expAt;
         payload["iat"] = secondsSinceEpoch;
      }

      log.debug({
         title:'payload',
         details: JSON.stringify(payload)});

      var header = encode.convert({
         string: JSON.stringify({
            type:'JWT',
            alg:'RS256'
         }),
         inputEncoding: encode.Encoding.UTF_8,
         outputEncoding: encode.Encoding.BASE_64_URL_SAFE
      }).replace(/=+$/, '');

      var body = encode.convert({
         string: JSON.stringify(payload),
         inputEncoding: encode.Encoding.UTF_8,
         outputEncoding: encode.Encoding.BASE_64_URL_SAFE}).replace(/=+$/, '');

      var signer = cert.createSigner({
         certId:'custcertificate_sample', //from Setup -> Company -> Certificates
         algorithm: cert.HashAlg.SHA256
      });

      signer.update(header +'.'+ body);

      var sig = signer.sign({
         outputEncoding:encode.Encoding.BASE_64_URL_SAFE
      }).replace(/=+$/, '');

      return [header, body, sig].join('.');
   }

   return {
      signIt: signIt
   }
});

The uploaded key was generated like:

openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -out public.pem -pubout
openssl req -key private.pem -new -x509 -days 3650 -subj "/C=CA/ST=Courtenay/O=Rule of Tech/OU=Information unit/CN=jwt.kotn.com" -out cert.pem

and then I just used a text editor to concatenate the cert and private key from cert.pem and private.pem:

-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
bknights
  • 14,408
  • 2
  • 18
  • 31
  • 1
    Seems like I might need some additional steps with this method. I tried to upload a PEM file for use in a certificate, but when I try to create the cert record it gives an error that says: `File does not contain X509 certificate`. Also, DocuSign seems to want both the public and private keys used in creating the certificate, bot not sure how much that really has to happen.. – Michael McCauley Aug 10 '21 at 21:30
  • 1
    Ok, I solved that weird little issue by installing OpenSSL and creating the necessary X509 certificate. I'll see how this ends up working for me. – Michael McCauley Aug 10 '21 at 22:04
  • This was a conversion from and SS1 version. I hadn't run it it Netsuite. Code updated to be working, verified JWT generator. Plus added key gen that I used – bknights Aug 10 '21 at 23:08
  • 1
    I was able to verify that it was creating valid JWTs. The issue I'm having now is that DocuSign is responding back with code 400, saying bad request. – Michael McCauley Aug 12 '21 at 21:49
  • Are you following this process: https://developers.docusign.com/platform/auth/jwt/jwt-get-token/ ? – bknights Aug 12 '21 at 22:00
  • I am. But I realized that I accidentally set an incorrect URL for the token request. Now I have a different issue with the correct URL... `Code: 400 , message body: {"error":"invalid_grant","error_description":"unsupported_grant_type"}`. Not sure what the deal is, there, though, as I'm sending this as the POST payload: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={JWT}` – Michael McCauley Aug 12 '21 at 22:41
  • you might want to play with the url encoding of the post. – bknights Aug 12 '21 at 23:17
  • 1
    I got it all figured out now. I had to fix some things I did in the JWT creation to test it out and revert them back to correct values. Apparently I also had to re-do the given consent, as well, and then it all worked appropriately. A little concerned that I had to give consent again, though. That could be problematic for the listener endpoint. – Michael McCauley Aug 12 '21 at 23:29
  • 1
    great! Happy to help – bknights Aug 12 '21 at 23:29