Generate an HMAC-SHA256 signature in Jitterbit Studio
Introduction
Many APIs authenticate a request by requiring a keyed hash of the request payload. The caller combines the payload with a shared secret using the HMAC (hash-based message authentication code) construction and sends the resulting signature in a request header. The receiving system recomputes the signature from the payload it received and accepts the request only if the two values match. This confirms both that the payload was not altered in transit and that the caller holds the secret. Inbound webhooks use the same mechanism in the opposite direction: the provider signs the payload it sends you, and your endpoint recomputes the signature to confirm the request is genuine.
This guide covers how to compute an HMAC-SHA256 signature in a Studio project using a JavaScript script, and how to use the resulting signature in an outbound request or to validate an inbound one.
The signature is computed in JavaScript rather than in a Jitterbit Script. An HMAC is not a hash of the secret and the payload joined together: the construction first derives two keys from the secret by flipping bits within each of its bytes, then runs SHA-256 twice, once with each derived key. Performing that calculation requires the bitwise operators ^, &, <<, and >>>, which JavaScript provides. The SHA256 function performs a plain hash, which is a separate calculation: hashing the secret and the payload as a single concatenated string yields a different value than the HMAC signature the receiving system expects.
The script produces a 64-character lowercase hexadecimal string that matches the value produced by any standard HMAC-SHA256 implementation for the same payload and secret, so a signature generated this way is interchangeable with one generated by an external tool or library.
Use this pattern where a project would otherwise call the deprecated HMAC-SHA256 Generator or HMAC-SHA1 Generator plugins through RunPlugin. For hashing tasks that do not involve a secret, such as change detection or fingerprinting records, use the SHA256 function directly, as described in Detect and deduplicate records using hash functions.
This guide assumes the following:
- You are familiar with creating scripts as project components.
- An operation exists that makes the outbound API call, or that is exposed as an API endpoint to receive inbound webhooks.
Note
To sign a JSON Web Token with HS256, use the JWT connector or the JWT functions instead. Those produce a complete token rather than a standalone signature. For a complete example, see Authenticate API endpoints using JWT.
Design pattern
The signing logic lives in its own script component so that any operation in the project can call it:
(set payload and secret,
call the signing script)"] --> B["HTTP v2 activity
(send the signed request)"]
A Jitterbit Script sets the payload and the secret as global variables, then calls the JavaScript signing script with RunScript. The signing script writes the signature to a third global variable, which the rest of the operation reads.
Global variables are the only way to pass values into a script written in JavaScript, as arguments included in a RunScript call are not available to it. Avoid periods in the names of global variables that a JavaScript script reads or writes, as described under Global variables.
Part 1: Store the shared secret
Store the signing secret as a project variable rather than hardcoding it in a script, so that its value can differ per environment and does not appear in operation logs.
-
In Studio, open the project actions menu and select Project Variables.
-
Add a variable named
api_signing_secretand enter the secret provided by the target system. -
Enable Hide value.
-
Click Save.
For the full steps and additional practices for handling credentials, see Manage endpoint credentials.
Part 2: Create the signing script
-
Create a new script in the project. The new script opens in the script editor.
-
Click the script name in the top left and enter a name (for example,
Generate HMAC Signature). -
Open the Script Type menu and select JavaScript.
-
Enter the following script. It implements SHA-256 and the HMAC construction using only ECMAScript 5.1 features, the standard supported by the Harmony JavaScript engine.
The two assignments under
Jitterbit usageat the end of the script set a known payload and secret so that you can run the script on its own and confirm its output before wiring it into an operation. Part 3 replaces them.HMAC-SHA256 signing script
<javascript> /* * HMAC-SHA256 for Jitterbit JavaScript * ES5 compatible - no Node.js crypto dependency */ function utf8Bytes(str) { str = unescape(encodeURIComponent(str)); var bytes = []; for (var i = 0; i < str.length; i++) { bytes.push(str.charCodeAt(i)); } return bytes; } function rightRotate(value, amount) { return (value >>> amount) | (value << (32 - amount)); } function sha256(bytes) { var k = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ]; var h = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]; var data = bytes.slice(); var bitLength = data.length * 8; data.push(0x80); while ((data.length % 64) !== 56) { data.push(0); } // High 32 bits data.push(0, 0, 0, 0); // Low 32 bits data.push( (bitLength >>> 24) & 0xff, (bitLength >>> 16) & 0xff, (bitLength >>> 8) & 0xff, bitLength & 0xff ); for (var offset = 0; offset < data.length; offset += 64) { var w = new Array(64); var i; for (i = 0; i < 16; i++) { var j = offset + i * 4; w[i] = ((data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | data[j + 3]); } for (i = 16; i < 64; i++) { var s0 = rightRotate(w[i - 15], 7) ^ rightRotate(w[i - 15], 18) ^ (w[i - 15] >>> 3); var s1 = rightRotate(w[i - 2], 17) ^ rightRotate(w[i - 2], 19) ^ (w[i - 2] >>> 10); w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0; } var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3]; var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7]; for (i = 0; i < 64; i++) { var S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25); var ch = (e & f) ^ ((~e) & g); var temp1 = (hh + S1 + ch + k[i] + w[i]) | 0; var S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22); var maj = (a & b) ^ (a & c) ^ (b & c); var temp2 = (S0 + maj) | 0; hh = g; g = f; f = e; e = (d + temp1) | 0; d = c; c = b; b = a; a = (temp1 + temp2) | 0; } h[0] = (h[0] + a) | 0; h[1] = (h[1] + b) | 0; h[2] = (h[2] + c) | 0; h[3] = (h[3] + d) | 0; h[4] = (h[4] + e) | 0; h[5] = (h[5] + f) | 0; h[6] = (h[6] + g) | 0; h[7] = (h[7] + hh) | 0; } var result = []; for (var x = 0; x < h.length; x++) { result.push((h[x] >>> 24) & 0xff); result.push((h[x] >>> 16) & 0xff); result.push((h[x] >>> 8) & 0xff); result.push(h[x] & 0xff); } return result; } function bytesToHex(bytes) { var hex = ""; for (var i = 0; i < bytes.length; i++) { var value = bytes[i].toString(16); if (value.length < 2) { value = "0" + value; } hex += value; } return hex; } function hmacSha256(secret, message) { var blockSize = 64; var key = utf8Bytes(secret); if (key.length > blockSize) { key = sha256(key); } while (key.length < blockSize) { key.push(0); } var outerKey = []; var innerKey = []; for (var i = 0; i < blockSize; i++) { outerKey[i] = key[i] ^ 0x5c; innerKey[i] = key[i] ^ 0x36; } var messageBytes = utf8Bytes(message); var innerHash = sha256( innerKey.concat(messageBytes) ); var finalHash = sha256( outerKey.concat(innerHash) ); return bytesToHex(finalHash); } /* * Jitterbit usage */ $payload = "The quick brown fox jumps over the lazy dog"; $secret = "key"; var payload = $payload; var secret = $secret; $hmacSignature = hmacSha256(secret, payload); </javascript> -
In the script editor, use the Edit/Test toggle to select Test mode, then click Run test. Confirm that the
hmacSignatureglobal variable is set tof7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8. This is the published test value for HMAC-SHA256 with the payloadThe quick brown fox jumps over the lazy dogand the secretkey. For more information, see Test a script.
Part 3: Sign an outbound request
Step 1: Make the script reusable
In the signing script, replace the two lines that assign the sample payload and secret so that the script signs whatever the calling operation provides:
/*
* Jitterbit usage
*/
var payload = $payload;
var secret = $secret;
$hmacSignature = hmacSha256(secret, payload);
The rest of the script is unchanged.
Step 2: Call the script from the operation
Add a script step to the operation, ahead of the activity that sends the request. Unlike the signing script, this one uses Jitterbit Script, the default language for a new script. It builds the payload, reads the secret, and calls the signing script:
<trans>
// The payload must match, byte for byte, what the target system signs
$payload = JSONStringify($requestBody);
$secret = [api_signing_secret];
RunScript("<TAG>script:Generate HMAC Signature</TAG>");
WriteToOperationLog("Signature generated for a payload of " + Length($payload) + " characters");
</trans>
The signature is now in the hmacSignature global variable, and remains available to the rest of the operation and to any downstream operations in the chain.
Caution
Log the length of the payload or the outcome of the call, but not the secret or the signature itself. A signature is a credential for the request it signs.
Step 3: Send the signature with the request
In the activity that sends the request, add the signature as a request header. For an HTTP v2 activity, add a row to the Request Headers table using the header name that the target system expects, and enter [hmacSignature] as the value.
Header names differ by API. Check the target system's documentation for the header name, whether the signature must be hex or Base64, and whether it must carry a prefix (for example, sha256=) or be combined with a timestamp.
Step 4: Convert the signature to Base64 (optional)
The signing script returns a hex-encoded signature. If the target system expects Base64 instead, convert it with HexToBinary and Base64Encode after calling the signing script:
<trans>
$hmacSignatureBase64 = Base64Encode(HexToBinary($hmacSignature));
</trans>
For the sample payload and secret in Part 2, this returns 97yD9DBThCSxMpjmqm+xQ+9NWaFJRhdZl0edvC0aPNg=.
Part 4: Validate an inbound webhook signature
An operation exposed as an API Manager endpoint can use the same signing script to confirm that an inbound request came from the expected sender. Compute the signature over the request body and compare it against the value in the signature header:
<trans>
$payload = $jitterbit.api.request.body;
$secret = [api_signing_secret];
RunScript("<TAG>script:Generate HMAC Signature</TAG>");
$receivedSignature = Get("jitterbit.api.request.headers.X-Signature");
If($hmacSignature != $receivedSignature,
$jitterbit.api.response.status_code = 401;
$jitterbit.api.response = "Invalid signature";
Return();
);
</trans>
The Get function is required to read a header whose name contains a hyphen. A header name without a hyphen can be referenced directly as $jitterbit.api.request.headers.<name>. See jitterbit.api.request.headers.*.
Providers differ in what they sign and how they encode the result. Sign exactly the string the provider specifies, which is often the raw request body but may include a timestamp or other fields, and compare against the same encoding the provider sends. A provider that sends a Base64 signature or an uppercase hex signature requires the comparison value to be converted first.
Considerations
-
Payload size: A script written in JavaScript is subject to a limit of 50,000 loop iterations, counted across the whole script rather than per loop. The signing functions use approximately three iterations for each byte of the payload, so a payload on the order of 16 KB can approach the limit on its own. Smaller payloads can reach it where the script performs other work that loops, or where the payload contains characters outside the ASCII range, which take more than one byte each. Test with payloads representative of your integration rather than treating any particular size as a supported maximum. On private agents, the limit can be raised as described under Loop iterations.
-
Payload consistency: The signature covers the exact bytes signed. Whitespace, property order, and character encoding must match between the payload signed here and the payload the target system verifies. Build the payload once, sign that string, and send the same string as the request body.
-
Secret encoding: The script treats the secret as a UTF-8 string. If the target system provides the secret as Base64 or hex, decode it to its string form before assigning it to the
secretvariable. -
Script placement: JavaScript is available in scripts created as project components, not in scripts used within a transformation. To sign a value during a transformation, call the signing script from an operation step ahead of the transformation and map the resulting global variable.
Verify the integration
-
Run the signing script on its own using the sample payload and secret from Part 2 and confirm that the signature is
f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8. -
Substitute your own payload and secret and compare the result against an independent HMAC-SHA256 implementation. Any standard implementation returns the same value for the same inputs.
-
Deploy and run the operation and confirm that the target system accepts the request.
-
If the target system rejects the signature, check the following in order:
- The payload signed is byte-for-byte identical to the payload sent in the request body.
- The encoding matches what the target expects: hex or Base64, uppercase or lowercase.
- Any required prefix or timestamp component is included in the string being signed, in the order the target specifies.
- The secret matches the value configured in the target system, with no leading or trailing whitespace.
-
If the operation fails with a loop iteration error, check the payload size against the limit described under Considerations.