- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 11-05-2018 02:19 AM
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.
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.
And from that same page, this reminder for anyone with an existing integration that you need to adopt this technology pretty soon!
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)
Even though ServiceNow Developer API docs say you can use dataStream in this situation it’s not correct, at least not for Slack.
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:
#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.
- 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.
- Reconstruct the request body into a string
- concatenate all parameters as param=value¶m=value¶m=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.
- Create a “base string” according to Slack specs
- concatenate “v0:” + timestamp header + “:” + request body
- 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
- 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.
- 15,267 Views

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Well done! Thank you for working this to completion and sharing with the community.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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');
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Importing CryptoJS as a scoped script include to generate the HMAC would still be an option here, would it not?

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I still don't get anything for rawPostBody when I try this. My PDI for testing this is Kingston.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I'm currently on London.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I'm seeing the same behavior as Matt and I'm testing in Madrid
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Yeah I just tried London too. Nothing there.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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..
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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).
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
What you are sharing has already been solved. How are you encoding the payload?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.