The Now Platform® Washington DC release is live. Watch now!

Help
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Generating a proper WS-Security UsernameToken/PasswordDigest for Outgoing SOAP Calls?

Oliver D_sereck
Mega Expert

Hi everyone,

As we all know, ServiceNow supports OOTB Basic, Certificate and Mutual Authentication when using Outbound SOAP Calls. This is great for most but unfortunately I now have a product that absolutely must use the OASIS UsernameToken/PasswordDigest Authentication

I have been looking around and strangely, found nothing about this topic. Has really no one come into this situation or am I the only one not getting this to work?

Using SOAPUI, I can generate the header and paste that directly into the SOAP Message Function but obviously that expires (even when set very high) and isnt the best way to go about it. I would rather be able to generate the full nonce and digest myself.

Anyone done this before?

8 REPLIES 8

Oliver D_sereck
Mega Expert

I have figured it out and gotten my implementation to connect to the desired webservice using UseranmeToken/PasswordDigest.



jshashes got me the actual answers but also knowing that the output must be already come base64 encoded, not be encoded after the hex value is returned.



So for anyone looking for a way to create a ws-security header for outbound soap calls:


Encryption Library: https://github.com/h2non/jshashes/blob/master/hashes.js


Header builder: https://github.com/mhzed/tinysoap/blob/master/lib/soap.js



Hook it all together and remember, the digest order is nonce, created date/time and then password:



var digest = new Hashes.SHA1().b64(nonce+created+password);



Also base64 encoded the nonce:



GlideStringUtil.base64Encode(nonce)


Since I needed this myself, I have taken the time to convert the part I needed (SHA1+Base64) into a proper Script Include.


The Script Include has all the helper functions that were included in the original jshashes. Only some of them are actually being used in the SHA1/Base64 but might as well include them anyway if someone wants to expand and use other encryptions.



List of public functions:


utf8Encode(string)


utf8Decode(string)


safe_add(x,y)


bit_rol(number, count)


rstr2hex(input, hexcase)


str2rstr_utf16le(input)


str2rstr_utf16be(input)


binb2rstr(input)


binl2rstr(input)


rstr2binl(input)


rstr2binb(input)


rstr2any(input, encoding)


rstr2b64(input, b64pad)


Base64_encode(input)


Base64_decode(input)


SHA1(input, output)


getISODate(input)


buildSecurityHeader(created, expires, username, digest, nonce)



SHA1 accepts an output parameter (only "base64" at the moment) but is optional. Default output is always base64.


Calling is simple: new EncryptionHelper().SHA1("nonce + created + password");



I have also added the getISODate function as the date of the Security Header and Digest must be formatted to ISO-8601 (YYYY-MM-DDTHH:MM:SSZ)



And finally the actual Security Header Builder function.



Example for execution:


var eh = new EncryptionHelper();


var now = new Date();


var created = eh.getISODate( now );


var expires = eh.getISODate( new Date(now.getTime() + (1000 * 600)) );


var username = "test_username";


var password = "test_password";


var nonce = "test_nonce";


var digest = eh.SHA1(nonce+created+password);


var header = eh.buildSecurityHeader(created, expires, username, digest, nonce);



var s = new SOAPMessage('SOAPMESSAGE', 'SOAPMESSAGEFUNCTION');


s.setXMLParameter('securityHeader', header);


var response = s.post();



I have just added ${securityHeader} into my SOAP Message Function to be added with the setXMLParameter



Script Include:


// Authors: Tomas Aparicio, Paul Johnston, Angel Marin, Jeremy Lin


// Original Code can be found here: https://github.com/h2non/jshashes/blob/master/hashes.js


// Adapted for ServiceNow by Oliver Dösereck




var EncryptionHelper = Class.create();


EncryptionHelper.prototype = {



  utf8Encode: function(str) {


  var x, y, output = '',


  i = -1,


  l;



  if (str && str.length) {


  l = str.length;


  while ((i += 1) < l) {


  /* Decode utf-16 surrogate pairs */


  x = str.charCodeAt(i);


  y = i + 1 < l ? str.charCodeAt(i + 1) : 0;


  if (0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) {


  x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);


  i += 1;


  }


  /* Encode output as utf-8 */


  if (x <= 0x7F) {


  output += String.fromCharCode(x);


  } else if (x <= 0x7FF) {


  output += String.fromCharCode(0xC0 | ((x >>> 6) & 0x1F),


  0x80 | (x & 0x3F));


  } else if (x <= 0xFFFF) {


  output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),


  0x80 | ((x >>> 6) & 0x3F),


  0x80 | (x & 0x3F));


  } else if (x <= 0x1FFFFF) {


  output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),


  0x80 | ((x >>> 12) & 0x3F),


  0x80 | ((x >>> 6) & 0x3F),


  0x80 | (x & 0x3F));


  }


  }


  }


  return output;


  }, // utf8Encode



  utf8Decode: function(str) {


  var i, ac, c1, c2, c3, arr = [],


  l;


  i = ac = c1 = c2 = c3 = 0;



  if (str && str.length) {


  l = str.length;


  str += '';



  while (i < l) {


  c1 = str.charCodeAt(i);


  ac += 1;


  if (c1 < 128) {


  arr[ac] = String.fromCharCode(c1);


  i += 1;


  } else if (c1 > 191 && c1 < 224) {


  c2 = str.charCodeAt(i + 1);


  arr[ac] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));


  i += 2;


  } else {


  c2 = str.charCodeAt(i + 1);


  c3 = str.charCodeAt(i + 2);


  arr[ac] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));


  i += 3;


  }


  }


  }


  return arr.join('');


  }, // utf8Decode



  /* Add integers, wrapping at 2^32. This uses 16-bit operations internally to work around bugs in some JS interpreters. */


  safe_add: function(x, y) {


  var lsw = (x & 0xFFFF) + (y & 0xFFFF),


  msw = (x >> 16) + (y >> 16) + (lsw >> 16);


  return (msw << 16) | (lsw & 0xFFFF);


  }, // safe_add



  /* Bitwise rotate a 32-bit number to the left. */


  bit_rol: function(num, cnt) {


  return (num << cnt) | (num >>> (32 - cnt));


  }, // bit_rol



  /* Convert a raw string to a hex string */


  rstr2hex: function(input, hexcase) {


  var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef',


  output = '',


  x, i = 0,


  l = input.length;


  for (; i < l; i += 1) {


  x = input.charCodeAt(i);


  output += hex_tab.charAt((x >>> 4) & 0x0F) + hex_tab.charAt(x & 0x0F);


  }


  return output;


  }, // rstr2hex



  /* Encode a string as utf-16 */


  str2rstr_utf16le: function(input) {


  var i, l = input.length,


  output = '';


  for (i = 0; i < l; i += 1) {


  output += String.fromCharCode(input.charCodeAt(i) & 0xFF, (input.charCodeAt(i) >>> 😎 & 0xFF);


  }


  return output;


  }, // str2rstr_utf16le



  str2rstr_utf16be: function(input) {


  var i, l = input.length,


  output = '';


  for (i = 0; i < l; i += 1) {


  output += String.fromCharCode((input.charCodeAt(i) >>> 😎 & 0xFF, input.charCodeAt(i) & 0xFF);


  }


  return output;


  }, // str2rstr_utf16be



  /* Convert an array of big-endian words to a string */


  binb2rstr: function(input) {


  var i, l = input.length * 32,


  output = '';


  for (i = 0; i < l; i += 😎 {


  output += String.fromCharCode((input[i >> 5] >>> (24 - i % 32)) & 0xFF);


  }


  return output;


  }, // binb2rstr



  /* Convert an array of little-endian words to a string */


  binl2rstr: function(input) {


  var i, l = input.length * 32,


  output = '';


  for (i = 0; i < l; i += 😎 {


  output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);


  }


  return output;


  }, // binl2rstr



  /* Convert a raw string to an array of little-endian words. Characters >255 have their high-byte silently ignored. */


  rstr2binl: function(input) {


  var i, l = input.length * 8,


  output = Array(input.length >> 2),


  lo = output.length;


  for (i = 0; i < lo; i += 1) {


  output[i] = 0;


  }


  for (i = 0; i < l; i += 😎 {


  output[i >> 5] |= (input.charCodeAt(i / 😎 & 0xFF) << (i % 32);


  }


  return output;


  }, // rstr2binl



  /* Convert a raw string to an array of big-endian words. Characters >255 have their high-byte silently ignored. */


  rstr2binb: function(input) {


  var i, l = input.length * 8,


  output = Array(input.length >> 2),


  lo = output.length;


  for (i = 0; i < lo; i += 1) {


  output[i] = 0;


  }


  for (i = 0; i < l; i += 😎 {


  output[i >> 5] |= (input.charCodeAt(i / 😎 & 0xFF) << (24 - i % 32);


  }


  return output;


  }, // rstr2binb



  /* Convert a raw string to an arbitrary string encoding */


  rstr2any: function(input, encoding) {


  var divisor = encoding.length,


  remainders = [],


  i, q, x, ld, quotient, dividend, output, full_length;



  /* Convert to an array of 16-bit big-endian values, forming the dividend */


  dividend = Array(Math.ceil(input.length / 2));


  ld = dividend.length;


  for (i = 0; i < ld; i += 1) {


  dividend[i] = (input.charCodeAt(i * 2) << 😎 | input.charCodeAt(i * 2 + 1);


  }



  /*


  Repeatedly perform a long division. The binary array forms the dividend,


  the length of the encoding is the divisor. Once computed, the quotient


  forms the dividend for the next step. We stop when the dividend is zerHashes.


  All remainders are stored for later use. */


  while (dividend.length > 0) {


  quotient = [];


  x = 0;


  for (i = 0; i < dividend.length; i += 1) {


  x = (x << 16) + dividend[i];


  q = Math.floor(x / divisor);


  x -= q * divisor;


  if (quotient.length > 0 || q > 0) {


  quotient[quotient.length] = q;


  }


  }


  remainders[remainders.length] = x;


  dividend = quotient;


  }



  /* Convert the remainders to the output string */


  output = '';


  for (i = remainders.length - 1; i >= 0; i--) {


  output += encoding.charAt(remainders[i]);


  }



  /* Append leading zero equivalents */


  full_length = Math.ceil(input.length * 8 / (Math.log(encoding.length) / Math.log(2)));


  for (i = output.length; i < full_length; i += 1) {


  output = encoding[0] + output;


  }


  return output;


  }, // rstr2any



  /* Convert a raw string to a base-64 string */


  rstr2b64: function(input, b64pad) {


  var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',


  output = '',


  len = input.length,


  i, j, triplet;


  b64pad = b64pad || '=';


  for (i = 0; i < len; i += 3) {


  triplet = (input.charCodeAt(i) << 16) | (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) | (i + 2 < len ? input.charCodeAt(i + 2) : 0);


  for (j = 0; j < 4; j += 1) {


  if (i * 8 + j * 6 > input.length * 😎 {


  output += b64pad;


  } else {


  output += tab.charAt((triplet >>> 6 * (3 - j)) & 0x3F);


  }


  }


  }


  return output;


  }, // rstr2b64



  Base64_encode: function(input) {


  var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',


  pad = '=', // default pad according with the RFC standard


  url = false, // URL encoding support @todo


  utf8 = true; // by default enable UTF-8 support encoding


  var i, j, triplet,


  output = '',


  len = input.length;



  pad = pad || '=';


  input = (utf8) ? this.utf8Encode(input) : input;



  for (i = 0; i < len; i += 3) {


  triplet = (input.charCodeAt(i) << 16) | (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) | (i + 2 < len ? input.charCodeAt(i + 2) : 0);


  for (j = 0; j < 4; j += 1) {


  if (i * 8 + j * 6 > len * 😎 {


  output += pad;


  } else {


  output += tab.charAt((triplet >>> 6 * (3 - j)) & 0x3F);


  }


  }


  }


  return output;


  }, // Base64_encode



  Base64_decode: function(input) {


  var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',


  pad = '=', // default pad according with the RFC standard


  url = false, // URL encoding support @todo


  utf8 = true; // by default enable UTF-8 support encoding


  // var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';


  var i, o1, o2, o3, h1, h2, h3, h4, bits, ac,


  dec = '',


  arr = [];


  if (!input) {


  return input;


  }



  i = ac = 0;


  input = input.replace(new RegExp('\\' + pad, 'gi'), ''); // use '='



  do {


  // unpack four hexets into three octets using index points in b64


  h1 = tab.indexOf(input.charAt(i));


  i++;


  h2 = tab.indexOf(input.charAt(i));


  i++;


  h3 = tab.indexOf(input.charAt(i));


  i++;


  h4 = tab.indexOf(input.charAt(i));


  i++;



  bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;



  o1 = bits >> 16 & 0xff;


  o2 = bits >> 8 & 0xff;


  o3 = bits & 0xff;


  ac += 1;



  if (h3 === 64) {


  arr[ac] = String.fromCharCode(o1);


  } else if (h4 === 64) {


  arr[ac] = String.fromCharCode(o1, o2);


  } else {


  arr[ac] = String.fromCharCode(o1, o2, o3);


  }


  } while (i < input.length);



  dec = arr.join('');


  dec = (utf8) ? this.utf8Decode(dec) : dec;



  return dec;


  }, // Base64_decode



  SHA1: function(input, output) {


  var utf8 = true; // enable/disable utf8 encoding


  var b64pad = '='; // base-64 pad character. Defaults to '=' for strict RFC compliance



  switch (output) {


  case "base64":


  return this.rstr2b64(this._SHA1_rstr(input, utf8), b64pad);


  case "":


  return "";


  default:


  return this.rstr2b64(this._SHA1_rstr(input, utf8), b64pad);


  }


  }, // SHA1



  /* Calculate the SHA-512 of a raw string */


  _SHA1_rstr: function(s, utf8) {


  s = (utf8) ? this.utf8Encode(s) : s;


  return this.binb2rstr(this._SHA1_binb(this.rstr2binb(s), s.length * 8));


  }, // _SHA1_rstr



  /* Calculate the SHA-1 of an array of big-endian words, and a bit length */



  _SHA1_binb: function(x, len) {


  var i, j, t, olda, oldb, oldc, oldd, olde,


  w = Array(80),


  a = 1732584193,


  b = -271733879,


  c = -1732584194,


  d = 271733878,


  e = -1009589776;



  /* append padding */


  x[len >> 5] |= 0x80 << (24 - len % 32);


  x[((len + 64 >> 9) << 4) + 15] = len;



  for (i = 0; i < x.length; i += 16) {


  olda = a;


  oldb = b;


  oldc = c;


  oldd = d;


  olde = e;



  for (j = 0; j < 80; j += 1) {


  if (j < 16) {


  w[j] = x[i + j];


  } else {


  w[j] = this.bit_rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);


  }


  t = this.safe_add(this.safe_add(this.bit_rol(a, 5), this._SHA1_ft(j, b, c, d)), this.safe_add(this.safe_add(e, w[j]), this._SHA1_kt(j)));


  e = d;


  d = c;


  c = this.bit_rol(b, 30);


  b = a;


  a = t;


  }



  a = this.safe_add(a, olda);


  b = this.safe_add(b, oldb);


  c = this.safe_add(c, oldc);


  d = this.safe_add(d, oldd);


  e = this.safe_add(e, olde);


  }


  return Array(a, b, c, d, e);


  }, // _SHA1_binb



  /* Perform the appropriate triplet combination function for the current iteration */


  _SHA1_ft: function(t, b, c, d) {


  if (t < 20) {


  return (b & c) | ((~b) & d);


  }


  if (t < 40) {


  return b ^ c ^ d;


  }


  if (t < 60) {


  return (b & c) | (b & d) | (c & d);


  }


  return b ^ c ^ d;


  }, // _SHA1_ft



  /* Determine the appropriate additive constant for the current iteration */


  _SHA1_kt: function(t) {


  return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :


  (t < 60) ? -1894007588 : -899497514;


  }, // _SHA1_kt



  /* Return Date to be formated as ISO-8601 YYYY-MM-DDTHH:MM:SSZ */


  getISODate: function(d) {


  function pad(n){return n<10 ? '0'+n : n;}


  return d.getUTCFullYear()+'-'+ pad(d.getUTCMonth()+1)+'-'+ pad(d.getUTCDate())+'T'+ pad(d.getUTCHours())+':'+ pad(d.getUTCMinutes())+':'+ pad(d.getUTCSeconds())+'Z';


  }, // getISODate



  buildSecurityHeader: function(created, expires, username, digest, nonce) {


  var header = "";


  header = header + "<wsse:Security soapenv:mustUnderstand=\"1\" xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">";


  header = header + "<wsu:Timestamp wsu:Id=\"Timestamp-"+created+"\">";


  header = header + "<wsu:Created>"+created+"</wsu:Created>";


  header = header + "<wsu:Expires>"+expires+"</wsu:Expires>";


  header = header + "</wsu:Timestamp>";


  header = header + "<wsse:UsernameToken wsu:Id=\"SecurityToken-"+created+"\">";


  header = header + "<wsse:Username>"+username+"</wsse:Username>";


  header = header + "<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"+digest+"</wsse:Password>";


  header = header + "<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"+GlideStringUtil.base64Encode(nonce)+"</wsse:Nonce>";


  header = header + "<wsu:Created>"+created+"</wsu:Created>";


  header = header + "</wsse:UsernameToken>";


  header = header + "</wsse:Security>";


  return header;


  } // buildSecurityHeader


};





Fantastic work Oliver!!



I was in need for a SHA1 hash to be generated and this did the trick! ServiceNOW should add this to Global!



-Matt


Hi Matt,



Agreed - very informative thread from Oliver.


What do you have in mind when you say ServiceNOW should add this to Global?



Best Regards



Tony