Postman pre request script for http signature
To test calling the SVaaS api using postman, you need to add this pre request script.
This use two properties from the postman environment: privateKey
and keyId
whose values are given by the manager when you generate a key.
This script will only work with RSA+SHA256 keys!
This script relies on Forge.js library for the RSA signing, it's automatically downloaded and evaluated. This method is considered unsafe, but there is no RSA support in postman as of today. Use this at your own risk.
You don't need to specify an authorization mechanism in postman or any headers, this script will do that automatically.
// !!!! This script only works with a RSA key and a sha256 hash !!!!
// Load Forge library (used for RSA signature)
if (!pm.globals.has("forgeJS")) {
pm.sendRequest("https://raw.githubusercontent.com/loveiset/RSAForPostman/master/forge.js", (err, res) => {
if(err) {
console.log(err);
} else {
console.log('ForgeJS downloaded');
pm.globals.set("forgeJS", res.text());
eval(pm.globals.get("forgeJS"));
populateRequest();
}
});
} else {
eval(pm.globals.get('forgeJS'));
populateRequest();
}
// Functions for signature
function generateAuthorization(data, { headers, keyId, privateKey })
{
const normalized = normalizeData(data, { headers });
const stringData = stringifyNormalizedData(normalized);
const signature = sign(stringData, privateKey);
return `Signature keyId="${keyId}",algorithm="hs2019",headers="${headers.map(h => h.toLowerCase()).join(' ')}",signature="${signature}"`;
}
function normalizeData(data, config)
{
const headers = [];
for (const h of config.headers)
{
const hl = h.toLowerCase();
if (hl === '(request-target)')
{
headers.push({ name: '(request-target)', values: [data.method.toLowerCase() + ' ' + data.path] });
}
else
{
const hv = Object.entries(data.headers).find(v => v[0].toLowerCase() === hl);
if (!hv || hv[1] === undefined || hv[1] === null)
{
throw new Error('Missing header ' + h + ' in request');
}
if (headers.find(e => e.name == hl))
{
throw new Error('Tried to add the same header multiple times: ' + h);
}
headers.push({ name: hl, values: Array.isArray(hv[1]) ? hv[1].map(v => v.toString()) : [hv[1]?.toString()] });
}
}
return {
headers
};
}
function stringifyNormalizedData(data)
{
const components = new Array();
for (const h of data.headers)
{
const val = h.values.map(v => v.split(/\r?\n|\r/g).map(v => v.trim()).join(' ')).map(v => v.length == 0 ? ' ' : v).join(', ');
components.push(h.name + ': ' + val);
}
return components.join('\n');
}
function sign(data, privateKey)
{
const messageDigest = forge.md.sha256.create();
messageDigest.update(data, "utf8");
return forge.util.encode64(forge.pki.privateKeyFromPem(privateKey).sign(messageDigest));
}
function populateRequest() {
// Get the current timestamp in milliseconds
const currentTimestamp = Date.now();
// Set the X-Knot-Date header
pm.request.headers.add({
key: "X-Knot-Date",
value: currentTimestamp.toString()
});
// Set the Content-Type header
pm.request.headers.add({
key: "Content-Type",
value: "application/json"
});
// Set the X-Api-Key header
pm.request.headers.add({
key: "x-api-key",
value: pm.environment.get("keyId")
});
const url = pm.request.url.toString(); // Get the full request URL
const path = url.split(pm.request.url.getHost())[1] || '/';
// Generate authorization header
const authorizationHeader = generateAuthorization({
headers: pm.request.headers.toObject(),
method: pm.request.method || 'POST',
path
}, {
headers: ['x-knot-date', '(request-target)', 'content-type'], // Specify signed headers
keyId: pm.environment.get('keyId'), // Retrieve keyId from Postman environment
privateKey: pm.environment.get('privateKey') // Retrieve private key from Postman environment
});
// Add the Authorization header to the request
pm.request.headers.add({
key: 'Authorization',
value: authorizationHeader
});
}