ServiceNow Unable to Process the Request Body with a Content-Type 'application/x-www-form-urlencoded' while using Scripted REST Web Service

surendarm
Kilo Expert

Hello All,

Scenario:

We trying to connect with a third party application where they can consume our API and send us the request to be processed and update the incident record.

Issue:

The Content-Type of the request body is 'application/x-www-form-urlencoded' and ServiceNow is unable to process the request.

  • Based on the link, we can override the 'Content-Type' to application/x-www-form-urlencoded
  • But, we aren't able to receive the body using 'datastream'. We have used this same script by referring the link
  • When we print the body, it shows 'com.glide.communications.GlideScriptableInputStream' in the logs

Kindly let me know if anyone has worked on processing the request body with the Content-Type as 'application/x-www-form-urlencoded'.

Thanks in advance.

Regards,

Surendar M

17 REPLIES 17

Matt Hernandez
Tera Guru

I ran into this same issue trying to update a Slack integration to the new "signing secret" instead of the deprecated "verification token." With 'x-www-form-urlencoded' I cannot use anything from the request.body object, and I'm trying to recreate the original request body by concatenating all the query parameters.

This may not solve the whole problem of my "signing secret" use case because the Slack docs say the body they send should be kept "raw" and hashed exactly as it arrived, so it would be a lot simpler if one of the request.body.data properties was compatible with 'x-www-form-urlencoded'.

I still have more to do for my use case, but this code might help somebody trying to parse queryParams. I didn't find anything on community showing how to parse queryParams when you don't know the parameter names ahead of time.

I'm literally at the exact place you are: able to process the payload elements and to form a url encoded string by processing each param through encodeURIComponent, but am only able to get so so results when attempting to hash and match with the signature passed by Slack. It's getting very frustrating, to be honest, because the way the request data comes through differs according to the callback trigger. For event API you can just process the request body as is, but for incoming Slash commands (e.g, "/incident create") the request data must be in a different form before it arrives into my REST script because I cannot for the life of me get the computed hash to match Slack's.

I'm a little behind, haven't moved any further because it was just recreational testing, but my experiment was also with an incoming Slash command. I used a Servicenow class called CertificateEncryption, but didn't yet search out any library to use for the "hexdigest" part. I'm basically doing (shown below) so far. But I won't really know until I do "hexdigest" just how close I am getting to a signature match.

EDIT: I tried GlideDigest from the docs.servicenow.com, but it didn't help.

EDIT: I gave up on CertificateEncryption and implemented CryptoJS, creating a script include (further down this thread). It computes the correct signature when used with sample request body from Slack site, but it doesn't produce a matching sig when used with my concatenated queryParams. I believe something is wrong with that process. The only difference between my 'crafted' request body of params and the working Slack example was the order of params and the extra 'api' param in my Slash request.

EDIT: I got it to work with two fixes, (1) Reorder the query parameters. My original loop gets them in the wrong order, which sabotages the ability to hash the request body. (2) After Encoding Uri, you must also replace %20 with a plus sign '+' as this is a requirement with the content type of 'application/x-www-form-urlencoded'.

var timestamp = request.getHeader('X-Slack-Request-Timestamp');
var slack_signature = request.getHeader('X-Slack-Signature');
var slack_signing_secret = gs.getProperty('x_44121_slack.slack_app_secret');

var input = "";

/*
    REM THIS METHOD OF QUERY PARAMS, ALTHOUGH THIS RETRIEVES, IT DOESNT GET THEM IN THE ORDER OF THE ORIGINAL REQUEST, SO IT BREAKS REQUEST SIGNATURE HASHING.

    var qparams = request.queryParams;
    for (var p in qparams) {
        for (var key in qparams[p]) {
            if (qparams[p].hasOwnProperty(key)) {
                var currentLine = p + "=" + encodeURIComponent(qparams[p][key]);
                input += (input == "" ? currentLine : "&" + currentLine);
            }
        }
    }
*/

/*
    HARD CODING THEM INTO THIS ORDER WORKED CORRECTLY FOR MY SLASH COMMAND.
    OBVIOUSLY THIS ASSUMES YOU KNOW THE REQUEST FORMAT AHEAD OF TIME.
 */
var qarray = [];    
qarray.push( { name: "token", value: request.queryParams.token[0] } );
qarray.push( { name: "team_id", value: request.queryParams.team_id[0] } );
qarray.push( { name: "team_domain", value: request.queryParams.team_domain[0] } );
qarray.push( { name: "channel_id", value: request.queryParams.channel_id[0] } );
qarray.push( { name: "channel_name", value: request.queryParams.channel_name[0] } );
qarray.push( { name: "user_id", value: request.queryParams.user_id[0] } );
qarray.push( { name: "user_name", value: request.queryParams.user_name[0] } );
qarray.push( { name: "command", value: request.queryParams.command[0] } );
qarray.push( { name: "text", value: request.queryParams.text[0] } );
qarray.push( { name: "response_url", value: request.queryParams.response_url[0] } );
qarray.push( { name: "trigger_id", value: request.queryParams.trigger_id[0] } );

for (var q = 0; q < qarray.length; q++) {
    var currentLine = qarray[q].name + "=" + encodeURIComponent(qarray[q].value);
    /*  
        QUOTE FROM: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
        For application/x-www-form-urlencoded, spaces are to be replaced by '+', 
        so one may wish to follow a encodeURIComponent replacement with an additional replacement of "%20" with "+". 
    */
    currentLine = currentLine.replace("%20", "+");
    input += (input == "" ? currentLine : "&" + currentLine);
}

var my_signature = "":
var sig_basestring = "";
var arrSecret = [];
arrSecret.push("v0");
arrSecret.push(timestamp);
arrSecret.push(input); //this is the Url encoded query params
sig_basestring = arrSecret.join(":");

// Use my script include of the Google archive, CryptoJS v3.1.2
var hash = CryptoJS.HmacSHA256(sig_basestring, slack_signing_secret);
if (hash) {
    gs.info("hash:" + hash);

    // CryptoJS hashing includes the hexdigest already, just return hash    
    my_signature = "v0=" + hash;
}

if (my_signature == slack_signature) {
    gs.info("The signatures " + my_signature + " matches Slack sig: " + slack_signature);
} else {
    gs.info("The signatures " + my_signature + " FAILS matching to Slack sig: " + slack_signature);
}

I found some success with responding to interactive messages (like hitting an "Approval" button in a Slack message) by doing the following:

var request_body = 'payload=' + encodeURIComponent(request.queryParams.payload.toString());
var arrSecret = [];
arrSecret.push("v0");
arrSecret.push(timestamp);
arrSecret.push(request_body); //this is the Url encoded query params
sig_basestring = arrSecret.join(":");

The request coming from my Slash command doesn't have a payload parameter.