Skip to content

Instantly share code, notes, and snippets.

@voituk
Created October 15, 2019 07:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save voituk/1420215e830021ab8d73cf3e4a56473d to your computer and use it in GitHub Desktop.
Save voituk/1420215e830021ab8d73cf3e4a56473d to your computer and use it in GitHub Desktop.
Simple HMAC URL signature & Verification in JavaScript (node)
#!/usr/bin/env node
const URL = require('url').URL,
crypto = require('crypto');
/**
*
* @param string url
* @param string secret
* @param string singParamName
*
* @returns String
*
* @throws Exception|TypeError if URL is not valid/parseable
*/
function signUrl(url, secret, singParamName = 'sign') {
const u = new URL(url);
u.searchParams.sort();
//console.log(u);
const signValue = crypto
.createHmac('sha1', secret)
.update(u.pathname + u.search)
.digest('base64')
.slice(0, 10)
u.searchParams.append(singParamName, signValue);
//console.log(u.href)
return u.href
}
/**
*
* @param string url
* @param {*} secret
* @param {*} signParamName
*
* @returns boolean
*
* @throws Exception|TypeError if URL is not valid/parseable
*/
function verifyUrl(url, secret, signParamName = 'sign') {
const u = new URL(url);
const testValue = u.searchParams.get(signParamName);
if (!testValue)
return false; // No or empty signature
// Leave alone everything that goes after the signature
let pos = u.search.indexOf('?'+signParamName+'=');
if (pos < 0)
pos = u.search.indexOf('&'+signParamName+'=');
if (pos < 0)
return false; // No signature parameter found (should not happens btw)
u.search = u.search.substr(0, pos)
//sort the rest of values
u.searchParams.sort()
const signValue = crypto
.createHmac('sha1', secret)
.update(u.pathname + u.search)
.digest('base64')
.slice(0, 10);
return signValue === testValue
}
// ---====Primitive test Suite ====---
// Exception on malformed (non parseable) URL
const badUrl1 = 'http//booking.lemonone.comas/path/to?user_id=111&hello[]=moto1&hello[]=?adkjasdka'
try {
signUrl(badUrl1, SECRET_KEY)
verifyUrl(badUrl1, SECRET_KEY)
console.assert(false, "Bad URL should trigger exception on signUrl/verifyUrl call")
} catch (err) {
// Everyting is fine, don't thwo assertions
}
const SECRET_KEY = 'U-PoppaByla-Sobaka-On-Ee-Lubil-Onasiela-kusok-miasa-on-ee-ubil'
let testUrl = 'https://booking.lemonone.com/path/to?user_id=111&hello[]=moto2&hello[]=moto1&org_id=222#hash=77'
let testUrlSign = 'cWc6nLn0r9'
// Could parse valid URL
console.assert(signUrl(testUrl, SECRET_KEY) != null, "%s is NOT valid", testUrl);
// Signature exists in a signed URL and its correct
console.assert(
0 <= signUrl(testUrl, SECRET_KEY).indexOf('&sign=' + testUrlSign),
'Signature (%s) is not correct for url (%s)',
testUrlSign, testUrl
)
// Order of parameter names should not matter (WARNING! order of array-values should be correct!)
console.assert(
0 <= signUrl('https://booking.lemonone.com/path/to?org_id=222&hello[]=moto2&user_id=111&hello[]=moto1&#hash=77', SECRET_KEY)
.indexOf('&sign=' + testUrlSign),
'Signature (%s) is not correct for url (%s)',
testUrlSign, testUrl
)
// Hash should not matter
console.assert(
0 <= signUrl('https://booking.lemonone.com/path/to?org_id=222&hello[]=moto2&user_id=111&hello[]=moto1&#hash=77&23712371', SECRET_KEY)
.indexOf('&sign=' + testUrlSign),
'Signature (%s) is not correct for url (%s)',
testUrlSign, testUrl
)
let signedUrl;
// Verification should work
signedUrl = 'https://booking.lemonone.com/path/to?hello%5B%5D=moto2&hello%5B%5D=moto1&org_id=222&user_id=111&sign=cWc6nLn0r9#hash=77'
console.assert(
verifyUrl(signedUrl, SECRET_KEY, 'sign'),
"Url is correctly signed (%s)", signedUrl
);
// wrong secret key should trigger verification error
signedUrl = 'https://booking.lemonone.com/path/to?hello%5B%5D=moto2&hello%5B%5D=moto1&org_id=222&user_id=111&sign=cWc6nLn0r9#hash=77'
console.assert(
verifyUrl(signedUrl, 'xxx', 'sign') === false,
"Wrong secret key should trigger verification error"
);
// Order of query parameters should not matter
signedUrl = 'https://booking.lemonone.com/path/to?hello%5B%5D=moto2&org_id=222&hello%5B%5D=moto1&user_id=111&sign=cWc6nLn0r9#hash=77'
console.assert(
verifyUrl(signedUrl, SECRET_KEY, 'sign'),
"Order of query parameters should not matter"
);
// Whatever is after sign parameter should not matter
signedUrl = 'https://booking.lemonone.com/path/to?hello%5B%5D=moto2&org_id=222&hello%5B%5D=moto1&user_id=111&sign=cWc6nLn0r9&extra=hello#hash=77'
console.assert(
verifyUrl(signedUrl, SECRET_KEY, 'sign'),
"Whatever is after sign parameter should not matterr"
);
// URL with no query parameters should still work
shortUrl = 'https://booking.lemonone.com/path/to#hello=moto';
shortUrlSign = 'zJwYJOcIlK';
console.assert(
verifyUrl( signUrl(shortUrl, SECRET_KEY), SECRET_KEY),
"URL with no query parameters should still work"
)
// TODO: Implementation on time-based URL
// TODO: what is signName parameter already exists in URL?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment