Servicenow and SOAP outbound: WS-Security UsernameToken/PasswordDigest
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 hours ago
Hey folks,
I just wanted to quickly share this in case there is anyone else working with SOAP outbound and WS-security.
It took a fair amount of time and a lot trial and error but finally I was able to successfully implement the WSE tags inside the soap header tag.
A shout out to @Oliver D_sereck and his article about this very same subject, it trully helped me. Link to the article here.
And of course I wouldn't be able to achieve this without a lot of help from AI. 😓
So to keep things brief, I'll leave below the pieces of code and the SOAP message function header that you'll need to build header. With this pieces you should be able to copy, paste, alter some variables and you should be good to go.
SOAP Message function:
<soapenv:Header>
${securityInfo}
</soapenv:Header>
Scoped script include that builds the SOAP call and header:
Your_Fuction_Name: function() {
try {
var globalHelper = new global.Helper();
var getUserAndPassword = globalHelper.getUser();
var user = getUserAndPassword.user;
var password = getUserAndPassword.password;
var headerVariables = globalHelper.generateUsernameToken(user, password);
var s = new sn_ws.SOAPMessageV2('SOAP - message ', 'SOAP message function');
// Header
var securityHeader = '<wsse:Security 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">' +
'<wsu:Timestamp wsu:Id="' + headerVariables.tsId + '">' +
'<wsu:Created>' + headerVariables.created + '</wsu:Created>' +
'<wsu:Expires>' + headerVariables.expires + '</wsu:Expires>' +
'</wsu:Timestamp>' +
'<wsse:UsernameToken wsu:Id="' + headerVariables.utId + '">' +
'<wsse:Username>' + user + '</wsse:Username>' +
'<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + headerVariables.passwordDigest + '</wsse:Password>' +
'<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + headerVariables.nonce + '</wsse:Nonce>' +
'<wsu:Created>' + headerVariables.created + '</wsu:Created>' +
'</wsse:UsernameToken>' +
'</wsse:Security>';
s.setStringParameterNoEscape('securityInfo', securityHeader);
//Other fields you might need:
s.setStringParameterNoEscape(' ' ,' ')
var response = s.execute();
var status = response.getStatusCode();
var responseBody = response.getBody();
//Since it returns an XML , I find it easier to read it as a JSON object
var jsonBody = gs.xmlToJSON(responseBody)
return JSON.stringfy(jsonBody)
} catch (e) {
return JSON.stringify({
error: e,
message: e.message || e.getMessage()
});
}
Global Helper:
getUser: function() {
/**
* This function is here because the method .getDecryptedValue();
* is only available in the GLOBAL scope
*/
try {
var profileName = ' ' //Update the profile name here;
var gr = new GlideRecord('sys_auth_profile_basic');
gr.addQuery('name', profileName);
gr.query();
if (gr.next()) {
var userName = gr.getValue('username');
var password = gr.password.getDecryptedValue();
if (password) {
return {
user: userName,
password: password
};
}
}
return JSON.stringify({
error: 'username or password not found: "' + profileName + '".'
});
} catch (ex) {
gs.error("Error: " + ex.message);
}
},
generateUsernameToken: function(username, password) {
try {
var created = new GlideDateTime().getValue().replace(' ', 'T') + 'Z';
var expiresGdt = new GlideDateTime();
expiresGdt.addSeconds(60);
var expires = expiresGdt.getValue().replace(' ', 'T') + 'Z';
var nonceBytes = this._generateRandomBytes(16);
var nonceBase64 = this._base64EncodeBinary(nonceBytes)
var digestInput = nonceBytes + created + password;
var digestBytes = this._sha1(digestInput);
var passwordDigest = this._base64EncodeBinary(digestBytes);
return {
tsId: 'TS-' + this._randomId(9),
utId: 'UsernameToken-' + this._randomId(9),
created: created,
expires: expires,
nonce: nonceBase64,
passwordDigest: passwordDigest,
username: username
};
} catch (e) {
return {
status: 'error in generateUsernameToken ',
error: e
};
}
},
_generateRandomBytes: function(length) {
try {
var out = '';
for (var i = 0; i < length; i++) {
out += String.fromCharCode(Math.floor(Math.random() * 256));
}
return out;
} catch (e) {
return {
status: 'error in _generateRandomBytes',
error: e
};
}
},
_randomId: function(length) {
try {
var chars = 'ABCDEFGHIJ'+'K'+'LMNOPQRSTUVWXYZ0123456789';
var out = '';
for (var i = 0; i < length; i++) {
out += chars.charAt(Math.floor(Math.random() * chars.length));
}
return out;
} catch (e) {
return {
status: 'error in _randomId',
error: e
};
}
},
_base64EncodeBinary: function(input) {
var b64chars = 'ABCDEFGHIJ'+'K+'LMNOPQRSTUVWXYZabcdefghij'+'k'+'lmnopqrstuvwxyz0123456789+/';
var b64 = '';
var i = 0;
while (i < input.length) {
var c1 = input.charCodeAt(i++) & 0xFF;
if (i === input.length) {
b64 += b64chars.charAt(c1 >> 2);
b64 += b64chars.charAt((c1 & 3) << 4);
b64 += '==';
break;
}
var c2 = input.charCodeAt(i++) & 0xFF;
if (i === input.length) {
b64 += b64chars.charAt(c1 >> 2);
b64 += b64chars.charAt(((c1 & 3) << 4) | ((c2 & 0xF0) >> 4));
b64 += b64chars.charAt((c2 & 0x0F) << 2);
b64 += '=';
break;
}
var c3 = input.charCodeAt(i++) & 0xFF;
b64 += b64chars.charAt(c1 >> 2);
b64 += b64chars.charAt(((c1 & 3) << 4) | ((c2 & 0xF0) >> 4));
b64 += b64chars.charAt(((c2 & 0x0F) << 2) | ((c3 & 0xC0) >> 6));
b64 += b64chars.charAt(c3 & 0x3F);
}
return b64;
},
_sha1: function(input) {
function rotl(n, s) {
return ((n << s) | (n >>> (32 - s))) >>> 0;
}
var bitLen = input.length * 8;
input += String.fromCharCode(0x80);
while ((input.length % 64) !== 56) {
input += String.fromCharCode(0x00);
}
for (var i = 7; i >= 0; i--) {
var shift = i * 8;
var byteVal = (shift < 32) ? (bitLen >>> shift) & 0xFF : 0;
input += String.fromCharCode(byteVal);
}
var h0 = 0x67452301,
h1 = 0xEFCDAB89,
h2 = 0x98BADCFE,
h3 = 0x10325476,
h4 = 0xC3D2E1F0;
for (var chunkStart = 0; chunkStart < input.length; chunkStart += 64) {
var w = [];
for (var t = 0; t < 16; t++) {
var off = chunkStart + t * 4;
w[t] = ((input.charCodeAt(off) << 24) |
(input.charCodeAt(off + 1) << 16) |
(input.charCodeAt(off + 2) << 8) |
(input.charCodeAt(off + 3))) >>> 0;
}
for (t = 16; t < 80; t++) {
w[t] = rotl(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1);
}
var a = h0,
b = h1,
c = h2,
d = h3,
e = h4;
for (t = 0; t < 80; t++) {
var f, k;
if (t < 20) {
f = (b & c) | (~b & d);
k = 0x5A827999;
} else if (t < 40) {
f = b ^ c ^ d;
k = 0x6ED9EBA1;
} else if (t < 60) {
f = (b & c) | (b & d) | (c & d);
k = 0x8F1BBCDC;
} else {
f = b ^ c ^ d;
k = 0xCA62C1D6;
}
var temp = (rotl(a, 5) + f + e + k + w[t]) >>> 0;
e = d;
d = c;
c = rotl(b, 30);
b = a;
a = temp;
}
h0 = (h0 + a) >>> 0;
h1 = (h1 + b) >>> 0;
h2 = (h2 + c) >>> 0;
h3 = (h3 + d) >>> 0;
h4 = (h4 + e) >>> 0;
}
var digest = '';
var parts = [h0, h1, h2, h3, h4];
for (var p = 0; p < parts.length; p++) {
var hVal = parts[p];
digest += String.fromCharCode((hVal >>> 24) & 0xFF);
digest += String.fromCharCode((hVal >>> 16) & 0xFF);
digest += String.fromCharCode((hVal >>> 8) & 0xFF);
digest += String.fromCharCode(hVal & 0xFF);
}
return digest;
},
Hope it helps!