The CreatorCon Call for Content is officially open! Get started here.

Matt Hernandez
Tera Guru

Introduction

[UPDATE: In 2022, the main advice of this article (to use a processor instead of Scripted REST) has been deprecated for about the past 5 versions of ServiceNow. Eventually I learned that processors can be subject to performance issues. It is not recommended, especially for beginners, to approach it this way. I haven't tested Slack messaging in a long while so YMMV, but I can also say that Flow Designer and Integration Hub capabilities have progressed sufficiently to accommodate most needs. --Matt]
While there are some good blogs about integrating ServiceNow with Slack, I’m going to bring you a topic that gets very little, if any coverage: request authentication.

find_real_file.png

If you want proof that an incoming request is coming from Slack, the older method was Verification Tokens. When you first set up a webhook integration with Slack they’ll send you a request and ask you to parse out the token and send it back to them. That’s how you prove you control the endpoint that you’re asking them to notify. Then going forward, they’ll send that token value in every request so it lets you know that the request came from them.

Well, maybe. Technically it could mean that someone knows your token and is sending you a fake request and pretending to be Slack.

Request Authentication

Notice at the end of the image above it says that the Verification Token methodology is deprecated. Slack is moving to a more secure “request signing” idea that many sites are now using. This new method uses a timestamp and computed value that is specific to each request.

So how does it work? Basically, you set up a Signing Secret ahead of time, in the App Credentials settings of your Slack app. Then with each request Slack computes a hash of a current timestamp and the current request body, using your shared secret as the key. They call this computed hash the signature and they send it in the headers. When you receive the request, you extract their timestamp and request body, COMPUTE YOUR OWN HASH using the same algorithm and your secret as the key. If your computed value matches theirs, then you have decent proof that the request came from them. Because this method involves your secret, the exact request body, and a time value it’s a better indication that the request is authentic.

Here is a snapshot of the procedure lifted from the Slack API page, Verifying requests from Slack. That page also has a “Step-by-step walk-through for validating a request” with sample values you can shove into your code for testing, to verify that your hash algorithm works correctly. However at the time of this article, their example was too simplistic--having an empty message text. So their example side-steps any issues of Url encoding that you'll encounter in a request with complex message text.

find_real_file.png

 

And from that same page, this reminder for anyone with an existing integration that you need to adopt this technology pretty soon!

find_real_file.png

The Hashing Algorithm

For the signature Slack has chosen to use a “hash-based message authentication code” called HMAC-SHA256. Wikipedia tells us that an HMAC involves a cryptographic hash function and a secret key, and may be used to simultaneously verify both the data integrity and the authentication of a message. The cryptographic strength depends on the cryptographic strength of the hash function, the size of its hash output, and the size and quality of the key. (HMAC - Wikipedia)

I’m no expert on encryption. Sounds good to me.

Slack requires the “hex digest” version of this hash for use in the final comparison. So you need a library that creates an HMAC-SHA256 hash, accepting a key and a message, and dumping out a hexadecimal string representation.

The Obstacles

Adoption of this technology may not be trivial, depending on your integration architecture and familiarity with the various APIs. When you begin to implement Signing Secrets in your ServiceNow Slack integration, you may find it quite maddening at first. There are several obstacles to adopting this authentication mechanism that you’ll have to overcome.

#Issue 1: Content-Type in Scripted REST

The content-type used by Slack is not fully supported for scripted REST endpoints. Remember how you and Slack are agreeing to both compute a hash from the request body? Well in ServiceNow scripted REST, you don’t get access to the request body for content-type ‘application/x-www-form-urlencoded.’ You just don’t. The request body properties shown below don’t contain anything.

(UPDATE: Servicenow claims this content type is now supported as of London release. If true, then it might no longer be necessary to use a processor. I haven't tried to validate their claim. I added a London documentation link in the comments)

find_real_file.png

 

Even though ServiceNow Developer API docs say you can use dataStream in this situation it’s not correct, at least not for Slack.

find_real_file.png

But you need access to the request body for Signing Secrets to work. This means you must find a way to “rebuild” the raw request, at least the part that Slack uses for its hash. Slack warns us that we’ll have trouble if we don’t handle the request correctly:

find_real_file.png

#Issue 2: Nothing Native

ServiceNow doesn’t have a native hashing library to compute HmacSHA256 and return a hex digest. So it can’t accommodate Slack request signing. It does have some scoped classes in the vicinity of this topic, but they don’t quite fit. I’ll touch on them quickly.

CertificateEncryption: This class is able to “generate a hash for a certificate, sign data using a private key, and generate a message authentication code.” It accepts a key, and a flag saying that you want “HmacSHA256” algorithm. But it outputs the result in base64 format. The trouble is we need Hex format, and it doesn’t offer that. Performing hex digest on this base64 result is not the same as performing hex digest before making it into base64. Not useful for Slack. 

GlideDigest: This class is able to “create a message digest from strings or input streams using MD5, SHA1, or SHA256 hash algorithms.” This does not create an HMAC since it doesn’t accept a secret key. It just runs a string through an algorithm to produce a different version of it. Also not useful for Slack.

#Issue 3: Rearchitecture

Scripted REST API simply cannot access the request body of a Slack request. If you’re already using Scripted REST you’ll have to change that architecture completely and make Slack send its traffic to a Processor, which is a different API and a different endpoint. More details in the Solution section.

The Solution

To make this work in ServiceNow you’ll need to call a consulting company. What? You are a consulting company? Okay, then we better figure out how to do this!

Here is the basic approach you need in order to overcome the obstacles.

  1. Use a Processor
    • Because you cannot get the request body in the scripted REST API (RESTAPIRequest), since it’s not available for the content-type Slack uses. Also, if you enumerate request.queryParams they will come back in the wrong order, blowing any chance of matching a signature from Slack, who computed it from the request params in the correct order!
    • A processor uses a different API, g_request (GlideServletRequest). With this API, you can call g_request.getParameterNames() to return the query parameters in their actual order.
  2. Reconstruct the request body into a string
    • concatenate all parameters as param=value&param=value&param=value... while doing the following:
    • before concatenating, use encodeURIComponent() on the value
    • before concatenating, replace some special characters for this content-type (e.g. SPACE with a plus sign). There are several character replacements needed, see the processor sample code. Thanks to Nate Burnett for sample code on this point.
  3. Create a “base string” according to Slack specs
    • concatenate “v0:” + timestamp header + “:” + request body
  4. Use CryptoJS library to compute hash
    • I downloaded the zip of crypto-js v3.1.2 from Google code archives and made a script include out of the file: 'hmac-sha256.js' from the rollups folder. Once you’ve put CryptoJS into a script include, you can compute the signature with:
      • var hash = CryptoJS.HmacSHA256(base string, slack_signing_secret);
      • This value already comes out in a hex digest format!
    • Prepend “v0=” to your hash to make the proper format for a Slack signature
  1. Compare signatures
    • Check your computed signature against the 'X-Slack-Signature' header value sent from Slack. If they match then you’ve built your request authentication correctly! Once you’ve tested this out, you can confidently reject a request that doesn’t match your computed signature. The message is not authentic!
    • In the sample code section for the scoped script include, see my special note regarding Slack’s recommendation for comparing signatures.

Sample Code

There are many ways you could implement this but I’ll post an example of a scoped Processor using a scoped Script include to validate the Slack signature, then writing all the request parameters to a table *if* the signature matches. I doubt that folks would want to store the request in a table if it doesn’t validate, but that’s for you to decide on your own. I’ll also post the CryptoJS class, in case it somehow becomes unavailable.

Please Note: This demo code is designed for a Slash command, and I used a table with all the columns necessary to store the parameters. You could also just store the whole request body in a single ‘payload’ column and parse it with a business rule. That would be more a little more versatile if the content of the request changes in the future! To use this example you'll need to create a system property to store your secret, and update the code with your property and table names.

Scoped Processor

A processor is a separate endpoint, an alternative to scripted REST. You have to configure your Slack app/Slash command to point to the Url path of your processor. For a scoped processor, don’t forget to use your full scope prefix! And don’t forget to change the tablename variable to your table name!

// DEMO Processor for Slack 
(function process(g_request, g_response, g_processor) {

	var app = "my App";
	var tablename = "x_44121_slack_slash_command";
	var arrayUtil = new global.ArrayUtil();
	
	var paramBody = ""; //we will try to reconstruct the request body from params
	var urlParamList = g_request.getParameterNames();
	urlParamList = arrayUtil.ensureArray(urlParamList);
	for (var p = 0; p < urlParamList.length; p++) {
		var param = urlParamList[p];
		var value = "" + g_request.getParameter(param);
		var currentLine = param + "=" + encodeURIComponent(value);
		currentLine = currentLine.replace(/%20/g, "+")
                         .replace(/!/g, '%21')
                         .replace(/'/g, '%27')
                         .replace(/\(/g, '%28')
                         .replace(/\)/g, '%29')
                         .replace(/\*/g, '%2A');
		paramBody += (paramBody == "" ? currentLine : "&" + currentLine);
	}
	gs.info("Dump params: " + paramBody);

	var timestamp = g_request.getHeader('X-Slack-Request-Timestamp');
	var slack_signature = g_request.getHeader('X-Slack-Signature');
	var auth = new SlackAuthenticator();
	var isAuthentic = auth.validateSigningSecret(timestamp, slack_signature, paramBody);
	
	if (!isAuthentic) {
		gs.info('REST /reply: The Slack message is not authentic.');
	}else{
		gs.info('Storing request.');

		var record = new GlideRecord(tablename);
		//record.setValue("method",g_request.getMethod()); //fails
		record.setValue("querystring",g_request.getQueryString());
		record.setValue('x_slack_request_timestamp', timestamp);
		record.setValue('x_slack_signature', slack_signature);

		// we made this array earlier..
		for (var p2 = 0; p2 < urlParamList.length; p2++) {
			var parm = urlParamList[p2];
			var val = "" + g_request.getParameter(parm);
			record.setValue(formatToSNStandards(parm),val);
		}

		var urlheaderList = g_request.getHeaderNames();
		urlheaderList = arrayUtil.ensureArray(urlheaderList);
		for (var h = 0; h < urlheaderList.length; h++) {
			var header = urlheaderList[h];
			var headerValue = g_request.getHeader(header);
			record.setValue(formatToSNStandards(header),headerValue);	
		}
		record.insert();
	}

	// Add your code here
	g_response.setStatus(200);

})(g_request, g_response, g_processor);

function formatToSNStandards(str){
	str=str.replace("-", "_");
	//return "u_"+str; //REM since my scoped app didn't prefix the column names
	return str;
}

Scoped Script Include

Don't forget to change the system property name in this code, to match yours! Special note: Although I did a simple comparison of signatures using “==”, for even higher security Slack recommends you implement a custom “hmac compare” function instead of just using “==”. Apparently how quickly the comparison fails can be measured by a serious attacker, and gives him hints about how good his guesses are. So there exist special comparison functions which ‘pad’ the timing of a failed match, to obscure it.

var SlackAuthenticator = Class.create();
SlackAuthenticator.prototype = {
    initialize: function() {
    },

    log_count: 0,
    slack_signing_secret: gs.getProperty('x_44121_slack.slack_app_secret') || "",
	
	validateSigningSecret: function (timestamp, slack_signature, request_body) {
		if (this.slack_signing_secret == "") {
			this.log("Unable to retrieve property: slack_signing_secret");
			return false;
		}
		
		this.log("Script include received request body: " + request_body);

		var arrSecret = [];
		var my_signature;
		
		//basesetring = version + timestamp + body, use an array to join
		arrSecret.push("v0");
		arrSecret.push(timestamp);
		arrSecret.push(request_body);
		var sig_basestring = arrSecret.join(":");
		this.log("Basestring: " + sig_basestring);

		var hash = CryptoJS.HmacSHA256(sig_basestring, this.slack_signing_secret);
		if (hash) {
			// CryptoJS hashing includes the hexdigest already, just return hash	
			my_signature = "v0=" + hash;
		}
		
		if (my_signature == slack_signature) {
			this.log("The signature " + my_signature + " matches Slack sig: " + slack_signature);
			return true;
		}else{
			this.log("The signature " + my_signature + " --FAIL-- match with Slack sig: " + slack_signature);
			return false;
		}
	},

	log: function(text) {
		this.log_count += 1;
		gs.info(this.log_count + ":" + text, "SlackAuthenticator");
	},

    type: 'SlackAuthenticator'
};

The CryptoJS Class

The source of this code is discussed in ‘The Solution’ section earlier in this document. I put it in a scoped script include called CryptoJS.

/*
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))}})})();

Table for storing Requests

Set whatever length you think you need for the columns. I just guessed so I won’t bore you with my choices. If you decide to use a table like this, see the link in my Reference section on "How to integrate ServiceNow & Slack’s Slash Commands." That author gives good recommendations for securing this table since it will contain sensitive data!

TABLE:
	x_44121_slack_slash_command
COLUMNS:
	String token
	String team_id
	String team_domain
	String channel_id
	String channel_name
	String user_id
	String user_name
	String command
	String text
	Url response_url
	String trigger_id
	String method
	String querystring
	String host
	String user_agent
	String accept_encoding
	String accept
	String content_length
	String content_type
	String x_forwarded_proto
	String x_forwarded_host
	String x_forwarded_for
	String x_slack_request_timestamp
	String x_slack_signature

References

Finally, here are some pages that I used as references. The 'How to integrate..' shows the details of setting up for a processor on the Slack side.

Verifying requests from Slack

How to integrate ServiceNow & Slack’s Slash Commands

CryptoJS

Dave’s Sift API video

generate Amazon S3 pre-signed URL programmatically

Comments
jarodm
Mega Guru

Well done! Thank you for working this to completion and sharing with the community.

Nate Burnett1
Kilo Explorer

Very nice write-up! One thing about the call to encodeURIComponent in the processor, some special characters still get missed and the hash will fail. You will need to hand escape further, like so:

currentLine = currentLine.replace(/%20/g, "+")
                         .replace(/!/g, '%21')
                         .replace(/'/g, '%27')
                         .replace(/\(/g, '%28')
                         .replace(/\)/g, '%29')
                         .replace(/\*/g, '%2A');
Matt Hernandez
Tera Guru

Awesome tip. I only tested with very simple messages so I missed that. I"ll update the article as soon as I get some time to test.

Matt Hernandez
Tera Guru

Ok, I tested your code and that's a big improvement! I was not thorough enough with my test messages the first time! That fix is much appreciated!

Code in the article is updated.

Matt Hernandez
Tera Guru

UPDATE: As of London release, Servicenow claims that content type application/x-www-form-urlencoded is now fully supported in Scripted REST API. I have not tested it, but below I included the link from Release notes where it claims that request.queryParams has been enhanced.

https://docs.servicenow.com/bundle/london-application-development/page/integrate/custom-web-services...

jarodm
Mega Guru

Some progress on this... (We're inching closer)

I think the only missing link on this is how to get the hash. I successfully manually matched the Slack signature using the method below.

I have unsuccessfully tried using scoped versions of GlideDigest() and CertificateEncryption() to generate the hash in real time.

 

var timestamp = request.getHeader('X-Slack-Request-Timestamp');
var slackSig = request.getHeader('X-Slack-Signature');

var stream = request.body.dataStream;  
var reader = new GlideTextReader(stream);  
var rawPostBody = "";  
var ln= "";  
var i = 0;
while((ln = reader.readLine()) != null) {  
    rawPostBody += ln;  
    i++;
} 

var baseString = 'v0:'+timestamp+':'+rawPostBody;

(SRAPI Datastream code from Dave Slusher's blog post: here)

 

https://www.freeformatter.com/hmac-generator.html#ad-output

Nate M_
Kilo Contributor

Importing CryptoJS as a scoped script include to generate the HMAC would still be an option here, would it not?

jarodm
Mega Guru

This will absolutely work (I import JS libs into SIs all the time).

I would like to have a 100% native solution working. Importing someone else's code into a SI signs yourself up to manage and monitor that project for future updates and security bulletins.

If the next "Heartbleed"-type code vulnerability is in CryptoJS, and you've imported that on lots of your customers' instances, that wouldn't be very good. Especially if it wasn't documented and they didn't know about it.

JarodM

Nate M_
Kilo Contributor

Just an FYI in my testing when I do a call to reader.readLine() the return value is always null (the use case again is parsing application/x-www-form-urlencoded body). I've done this in both scoped and global resources, and it's the same result.

Matt Hernandez
Tera Guru

I still don't get anything for rawPostBody when I try this. My PDI for testing this is Kingston.

jarodm
Mega Guru

I'm currently on London.

Nate M_
Kilo Contributor

I'm seeing the same behavior as Matt and I'm testing in Madrid

Matt Hernandez
Tera Guru

Yeah I just tried London too. Nothing there.

Matt Hernandez
Tera Guru

It doesn't work for me on London. From reading Dave's article you linked, I got the impression maybe it worked for his situation, where he was dealing with an API that sends text/plain.

from the article:

>> I had been having trouble with a webhook that should have been hitting
>> my Scripted REST API endpoint but I had no evidence of it ever executing.
>> After examining the webhook transactions with the help of Mockbin I realized
>> that the webhook sends requests with Content-Type set to text/plain even though the body is XML.
>> Because the accept types were set to JSON or XML, this never even completed the handshake
>> ...
>> By adding text/plain to the allowed Content-Type, attempts to access request.body.data resulted in this error..

Nate M_
Kilo Contributor

Well if that's the case then that's disappointing, though by jarod's own screenshot I'm guessing not? This is all a complete mystery to me.

Ben Vargas
Tera Guru

New York release docs state creating new Processors are deprecated, I haven't had a chance to test reader.readLine() in New York to see if this gives us anything. Has anyone else tried this in New York yet?

Matt Hernandez
Tera Guru

I can verify in my dev instance on New York RTP patch0-hotfix1, you are still allowed to create a new processor. However I don't know about whether they have changed/fixed the ability in Scripted REST to access the incoming request parameters in their original order. I just don't have time to get into it at the moment, sorry.

Georgi Tabutov1
Giga Contributor

Just sharing my 2 cents. I don't know if this would be helpful but it worked for me.

I was able to get this up and running with Scripted REST API, without the use of processors. Retrieving the request.queryParams gets you the payload object containing an array of JSONs, in my particular case it was just one JSON (I am doing a message action->opening a dialog and then submitting the dialog as well as slash commands). The request.queryParams.payload contains your JSON, you just need to encode it and replace the special characters and concatenate it in the following format: 'v0:timestamp:payload=encodedJSON' this would provide the desired result.

Another thing which I don't think is stressed enough in the article is that you MUST import CryptoJS as a scoped Script Include as it will not work in the global scope.  

Nate M_
Kilo Contributor

Would you be willing to share the code? I have done as you described: encoding the payload as a JSON string, escaping the special characters, and I still cannot get a matching hash for the signature. I really believe that the payload as it comes into the Scripted Rest is not serialized correctly (the parameters are out of order).

Georgi Tabutov1
Giga Contributor

You can find the code below. Another resource which I have used to get to this point is: https://webhook.site, it's a mock webhook where you can sent the slack requests to see the exact request payload being sent. Something I couldn't get in ServiceNow. In the code below the 'payload' parameter passed to the 'authenticateSlackRequest' function is essentially the following: request.queryParams.payload in the ScriptedRestAPI.

_encodeRequestBody: function(payload, requestTimestamp){
		var requestJSON = encodeURIComponent(payload);
		requestJSON = requestJSON.replace(/%20/g, "+")
		.replace(/!/g, '%21')
		.replace(/'/g, '%27')
		.replace(/\(/g, '%28')
		.replace(/\)/g, '%29')
		.replace(/\*/g, '%2A');
		var arrSecret = [];
		arrSecret.push('v0');
		arrSecret.push(requestTimestamp);
		arrSecret.push('payload=' + requestJSON);
		return arrSecret.join(':');
	},
	authenticateSlackRequest: function(payload, requestTimestamp, signatureString){
		var hash = sn_slack_ah_v2.CryptoJS.HmacSHA256(this._encodeRequestBody(payload, requestTimestamp), gs.getProperty('slack.secret'));
		return this._checkSecret(hash, signatureString);
	},

Hope that helps.

Nate M_
Kilo Contributor

What you are sharing has already been solved. How are you encoding the payload? 

Georgi Tabutov1
Giga Contributor

Lets unpack this step by step.

Slack sends the following payload (I am using an interactive message button/action for this example): 

payload=%7B%22type%22%3A%22message_action%22%2C%22token%22%3A%22UGWR5i3labV0cCWwzg0Su7hA%22%2C%22action_ts%22%3A%221571844302.559125%22%2C%22team%22%3A%7B%22id%22%3A%22TM60L7RLY%22%2C%22domain%22%3A%22testslacksnow%22%7D%2C%22user%22%3A%7B%22id%22%3A%22ULZKS2J8H%22%2C%22name%22%3A%22georgi.tabutov%22%7D%2C%22channel%22%3A%7B%22id%22%3A%22CMVLLL1HT%22%2C%22name%22%3A%22trade_support%22%7D%2C%22callback_id%22%3A%22create_incident%22%2C%22trigger_id%22%3A%22806225982085.720020263712.06441061e49029bf8ec66cd824d961dc%22%2C%22message_ts%22%3A%221569239421.000600%22%2C%22message%22%3A%7B%22client_msg_id%22%3A%229996A3A2-0BF4-49BD-B240-5CF5AEA35C6E%22%2C%22type%22%3A%22message%22%2C%22text%22%3A%22Something+is+broken+please+fix%22%2C%22user%22%3A%22UMGB8UJMV%22%2C%22ts%22%3A%221569239421.000600%22%2C%22team%22%3A%22TM60L7RLY%22%7D%2C%22response_url%22%3A%22https%3A%5C%2F%5C%2Fhooks.slack.com%5C%2Fapp%5C%2FTM60L7RLY%5C%2F806217836256%5C%2Fr6xWdG2tQnm7rF0s3wVg2ykV%22%7D

So our goal is to get to a point where our encoded payload is the same as the string above.

if we decode everything after the 'payload=' it results to the following:

{ 
    "type":"message_action",
    "token":"UGWR5i3labV0cCWwzg0Su7hA",
    "action_ts":"1571844302.559125",
    "team":{ 
        "id":"TM60L7RLY",
        "domain":"testslacksnow"
    },
    "user":{ 
        "id":"ULZKS2J8H",
        "name":"georgi.tabutov"
    },
    "channel":{ 
        "id":"CMVLLL1HT",
        "name":"trade_support"
    },
    "callback_id":"create_incident",
    "trigger_id":"806225982085.720020263712.06441061e49029bf8ec66cd824d961dc",
    "message_ts":"1569239421.000600",
    "message":{ 
        "client_msg_id":"9996A3A2-0BF4-49BD-B240-5CF5AEA35C6E",
        "type":"message",
        "text":"Something+is+broken+please+fix",
        "user":"UMGB8UJMV",
        "ts":"1569239421.000600",
        "team":"TM60L7RLY"
    },
    "response_url":"https:\/\/hooks.slack.com\/app\/TM60L7RLY\/806217836256\/r6xWdG2tQnm7rF0s3wVg2ykV"
}

Conveniently this is a JSON, meaning all we need to do is to find this JSON and encode it and add 'payload=' to it (as there is nothing to encode in it).

Now in our ScriptedRestAPI we can retrieve the queryParams from the request. In the code below this would be the 'requestBody' variable. The 'requestBody' object contains a payload key. If we print 'requestBody.payload' we get:

 

{ 
    "type":"message_action",
    "token":"UGWR5i3labV0cCWwzg0Su7hA",
    "action_ts":"1571844302.559125",
    "team":{ 
        "id":"TM60L7RLY",
        "domain":"testslacksnow"
    },
    "user":{ 
        "id":"ULZKS2J8H",
        "name":"georgi.tabutov"
    },
    "channel":{ 
        "id":"CMVLLL1HT",
        "name":"trade_support"
    },
    "callback_id":"create_incident",
    "trigger_id":"806225982085.720020263712.06441061e49029bf8ec66cd824d961dc",
    "message_ts":"1569239421.000600",
    "message":{ 
        "client_msg_id":"9996A3A2-0BF4-49BD-B240-5CF5AEA35C6E",
        "type":"message",
        "text":"Something+is+broken+please+fix",
        "user":"UMGB8UJMV",
        "ts":"1569239421.000600",
        "team":"TM60L7RLY"
    },
    "response_url":"https:\/\/hooks.slack.com\/app\/TM60L7RLY\/806217836256\/r6xWdG2tQnm7rF0s3wVg2ykV"
}

So all we need to do is encode this and as mentioned above add 'payload=' to it. This is the difference between what I have done and what OP has done, as in his case he takes each parameter passed and processes it. In my case the entire JSON object is encoded and set to a payload parameter.

Please refer to the code below for how I extract the payload.

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
	
	var slackAPIInbound = new SchonfeldSlackAPIInbound();
	var slackAPIOutbound = new SchonfeldSlackAPIOutbound();
	var signatureString = request.getHeader('X-Slack-Signature');
	var requestTimestamp = request.getHeader('X-Slack-Request-Timestamp');
	var requestBody = request.queryParams;
	if(authenticateSlackRequest(requestBody.payload, requestTimestamp, signatureString))

Hope this works for you, if not paste your code and I can try to help. 

Nate M_
Kilo Contributor

Hi George,

Sorry to respond so late to this, but unfortunately I am nowhere closer to cracking this. And again I believe it's because ServiceNow is doing something to the incoming request. Your example above is for encoding an interactive response. Have you had success with a Slash Command? If so, how are you building the encoded payload? It appears the approach you take is to pull the "payload" element from the request query parameters, like so:

var requestBody = request.queryParams;
if(authenticateSlackRequest(requestBody.payload // snipping the rest

However, for Slash Commands this will not work as the "payload" element is undefined in this case. What I and I believe some of the others on this thread have attempted is something like this:

var requestParams = request.queryParams;
var rawPostBody = [];
for(var param in requestParams) {
	if (requestParams.hasOwnProperty(param)) {
		if (param.length != undefined)
			rawPostBody.push(param + "=" + requestParams[param][0]);
		else
			rawPostBody.push(param + "=" + requestParams[param]);
	}
}
var payload = rawPostBody.join('&');
if(authenticateSlackRequest(payload // snipping the rest

But this will result in a mismatch hash against the Slack one. This is why I suspect ServiceNow is mutating the request somehow before passing to the SRAPI. Hope that helps.

Version history
Last update:
‎11-05-2018 02:19 AM
Updated by: