Created
October 15, 2019 07:46
-
-
Save voituk/1420215e830021ab8d73cf3e4a56473d to your computer and use it in GitHub Desktop.
Simple HMAC URL signature & Verification in JavaScript (node)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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