How to decode jwt token in javascript without using a library? - javascript

How can I decode the payload of JWT using JavaScript? Without a library. So the token just returns a payload object that can consumed by my front-end app.
Example token: xxxxxxxxx.XXXXXXXX.xxxxxxxx
And the result is the payload:
{exp: 10012016 name: john doe, scope:['admin']}

Note: this does not validate the signature, it just extracts the JSON payload from the token, which could have been tampered with.
Browser
Working unicode text JWT parser function:
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
JWT uses base64url (RFC 4648 ยง5), so using only atob (which uses base64) isn't enough.
Node.js
function parseJwt (token) {
return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
}

Simple function with try - catch
const parseJwt = (token) => {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch (e) {
return null;
}
};
Thanks!

You can use jwt-decode, so then you could write:
import jwt_decode from 'jwt-decode';
var token = 'eyJ0eXAiO.../// jwt token';
var decoded = jwt_decode(token);
console.log(decoded);
/*{exp: 10012016 name: john doe, scope:['admin']}*/

you can use pure javascript atob() function to decode token into a string:
atob(token.split('.')[1]);
or parse directly it into a json object:
JSON.parse(atob(token.split('.')[1]));
read about atob() and btoa() built-in javascript functions Base64 encoding and decoding - Web APIs | MDN.

function parseJwt(token) {
var base64Payload = token.split('.')[1];
var payload = Buffer.from(base64Payload, 'base64');
return JSON.parse(payload.toString());
}
let payload= parseJwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");
console.log("payload:- ", payload);
If using node, you might have to use buffer package:
npm install buffer
var Buffer = require('buffer/').Buffer

As "window" object is not present in nodejs environment,
we could use the following lines of code :
let base64Url = token.split('.')[1]; // token you get
let base64 = base64Url.replace('-', '+').replace('_', '/');
let decodedData = JSON.parse(Buffer.from(base64, 'base64').toString('binary'));
It's working for me perfectly. Hope it helps.

If you're using Typescript or vanilla JavaScript, here's a zero-dependency, ready to copy-paste in your project simple function (building on #Rajan Maharjan 's answer).
This answer is particularly good, not only because it does not depend on any npm module, but also because it does not depend an any node.js built-in module (like Buffer) that some other solutions here are using and of course would fail in the browser (unless polyfilled, but there's no reason to do that in the first place). Additionally JSON.parse can fail at runtime and this version (especially in Typescript) will force handling of that. The JSDoc annotations will make future maintainers of your code thankful. :)
/**
* Returns a JS object representation of a Javascript Web Token from its common encoded
* string form.
*
* #template T the expected shape of the parsed token
* #param {string} token a Javascript Web Token in base64 encoded, `.` separated form
* #returns {(T | undefined)} an object-representation of the token
* or undefined if parsing failed
*/
export function getParsedJwt<T extends object = { [k: string]: string | number }>(
token: string,
): T | undefined {
try {
return JSON.parse(atob(token.split('.')[1]))
} catch {
return undefined
}
}
For completion, here's the vanilla javascript version too:
/**
* Returns a JS object representation of a Javascript Web Token from its common encoded
* string form.
*
* #param {string} token a Javascript Web Token in base64 encoded, `.` separated form
* #returns {(object | undefined)} an object-representation of the token
* or undefined if parsing failed
*/
export function getParsedJwt(token) {
try {
return JSON.parse(atob(token.split('.')[1]))
} catch (error) {
return undefined
}
}

If you use Node.JS,
You can use the native Buffer module by doing :
const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImU3YjQ0Mjc4LTZlZDYtNDJlZC05MTZmLWFjZDQzNzhkM2U0YSIsImlhdCI6MTU5NTg3NzUxOCwiZXhwIjoxNTk1ODgxMTE4fQ.WXyDlDMMSJAjOFF9oAU9JrRHg2wio-WolWAkAaY3kg4';
const tokenDecodablePart = token.split('.')[1];
const decoded = Buffer.from(tokenDecodablePart, 'base64').toString();
console.log(decoded)
And you're good to go :-)

#Peheje will work, but you will have problem with unicode.
To fix it I use the code on https://stackoverflow.com/a/30106551/5277071;
let b64DecodeUnicode = str =>
decodeURIComponent(
Array.prototype.map.call(atob(str), c =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join(''))
let parseJwt = token =>
JSON.parse(
b64DecodeUnicode(
token.split('.')[1].replace('-', '+').replace('_', '/')
)
)
let form = document.getElementById("form")
form.addEventListener("submit", (e) => {
form.out.value = JSON.stringify(
parseJwt(form.jwt.value)
)
e.preventDefault();
})
textarea{width:300px; height:60px; display:block}
<form id="form" action="parse">
<textarea name="jwt">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkrDtGhuIETDs8OoIiwiYWRtaW4iOnRydWV9.469tBeJmYLERjlKi9u6gylb-2NsjHLC_6kZNdtoOGsA</textarea>
<textarea name="out"></textarea>
<input type="submit" value="parse" />
</form>

I use this function to get payload , header , exp(Expiration Time), iat (Issued At) based on this answer
function parseJwt(token) {
try {
// Get Token Header
const base64HeaderUrl = token.split('.')[0];
const base64Header = base64HeaderUrl.replace('-', '+').replace('_', '/');
const headerData = JSON.parse(window.atob(base64Header));
// Get Token payload and date's
const base64Url = token.split('.')[1];
const base64 = base64Url.replace('-', '+').replace('_', '/');
const dataJWT = JSON.parse(window.atob(base64));
dataJWT.header = headerData;
// TODO: add expiration at check ...
return dataJWT;
} catch (err) {
return false;
}
}
const jwtDecoded = parseJwt('YOUR_TOKEN') ;
if(jwtDecoded)
{
console.log(jwtDecoded)
}

If using node.js 16 or later, you can use the built-in base64url encoder/decoder.
let payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url"));

I found this code at jwt.io and it works well.
//this is used to parse base64
function url_base64_decode(str) {
var output = str.replace(/-/g, '+').replace(/_/g, '/');
switch (output.length % 4) {
case 0:
break;
case 2:
output += '==';
break;
case 3:
output += '=';
break;
default:
throw 'Illegal base64url string!';
}
var result = window.atob(output); //polifyll https://github.com/davidchambers/Base64.js
try{
return decodeURIComponent(escape(result));
} catch (err) {
return result;
}
}
In some cases(certain development platforms),
the best answer(for now) faces a problem of invalid base64 length.
So, I needed a more stable way.
I hope it would help you.

You can define & use this one liner func:
jwtDecode = b => JSON.parse(Buffer.from(b.split('.')[1], 'base64').toString('binary'));

all features of jwt.io doesn't support all languages. In NodeJs you can use
var decoded = jwt.decode(token);

Answer based from GitHub - auth0/jwt-decode. Altered the input/output to include string splitting and return object { header, payload, signature } so you can just pass the whole token.
var jwtDecode = function (jwt) {
function b64DecodeUnicode(str) {
return decodeURIComponent(atob(str).replace(/(.)/g, function (m, p) {
var code = p.charCodeAt(0).toString(16).toUpperCase();
if (code.length < 2) {
code = '0' + code;
}
return '%' + code;
}));
}
function decode(str) {
var output = str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
case 0:
break;
case 2:
output += "==";
break;
case 3:
output += "=";
break;
default:
throw "Illegal base64url string!";
}
try {
return b64DecodeUnicode(output);
} catch (err) {
return atob(output);
}
}
var jwtArray = jwt.split('.');
return {
header: decode(jwtArray[0]),
payload: decode(jwtArray[1]),
signature: decode(jwtArray[2])
};
};

Simple NodeJS Solution for Decoding a JSON Web Token (JWT)
function decodeTokenComponent(value) {
const buff = new Buffer(value, 'base64')
const text = buff.toString('ascii')
return JSON.parse(text)
}
const token = 'xxxxxxxxx.XXXXXXXX.xxxxxxxx'
const [headerEncoded, payloadEncoded, signature] = token.split('.')
const [header, payload] = [headerEncoded, payloadEncoded].map(decodeTokenComponent)
console.log(`header: ${header}`)
console.log(`payload: ${payload}`)
console.log(`signature: ${signature}`)

In Node.js (TypeScript):
import { TextDecoder } from 'util';
function decode(jwt: string) {
const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split('.');
if (length !== 3) {
throw new TypeError('Invalid JWT');
}
const decode = (input: string): JSON => { return JSON.parse(new TextDecoder().decode(new Uint8Array(Buffer.from(input, 'base64')))); };
return { header: decode(encodedHeader), payload: decode(encodedPayload), signature: signature };
}
With jose by panva on GitHub, you could use the minimal import { decode as base64Decode } from 'jose/util/base64url' and replace new Uint8Array(Buffer.from(input, 'base64')) with base64Decode(input). Code should then work in both browser and Node.js.

Here is a more feature-rich solution I just made after studying this question:
const parseJwt = (token) => {
try {
if (!token) {
throw new Error('parseJwt# Token is required.');
}
const base64Payload = token.split('.')[1];
let payload = new Uint8Array();
try {
payload = Buffer.from(base64Payload, 'base64');
} catch (err) {
throw new Error(`parseJwt# Malformed token: ${err}`);
}
return {
decodedToken: JSON.parse(payload),
};
} catch (err) {
console.log(`Bonus logging: ${err}`);
return {
error: 'Unable to decode token.',
};
}
};
Here's some usage samples:
const unhappy_path1 = parseJwt('sk4u7vgbis4ewku7gvtybrose4ui7gvtmalformedtoken');
console.log('unhappy_path1', unhappy_path1);
const unhappy_path2 = parseJwt('sk4u7vgbis4ewku7gvtybrose4ui7gvt.malformedtoken');
console.log('unhappy_path2', unhappy_path2);
const unhappy_path3 = parseJwt();
console.log('unhappy_path3', unhappy_path3);
const { error, decodedToken } = parseJwt('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c');
if (!decodedToken.exp) {
console.log('almost_happy_path: token has illegal claims (missing expires_at timestamp)', decodedToken);
// note: exp, iat, iss, jti, nbf, prv, sub
}
I wasn't able to make that runnable in StackOverflow code snippet tool, but here's approximately what you would see if you ran that code:
I made the parseJwt function always return an object (to some degree for static-typing reasons).
This allows you to utilize syntax such as:
const { decodedToken, error } = parseJwt(token);
Then you can test at run-time for specific types of errors and avoid any naming collision.
If anyone can think of any low effort, high value changes to this code, feel free to edit my answer for the benefit of next(person).

Based on answers here and here:
const dashRE = /-/g;
const lodashRE = /_/g;
module.exports = function jwtDecode(tokenStr) {
const base64Url = tokenStr.split('.')[1];
if (base64Url === undefined) return null;
const base64 = base64Url.replace(dashRE, '+').replace(lodashRE, '/');
const jsonStr = Buffer.from(base64, 'base64').toString();
return JSON.parse(jsonStr);
};

An es-module friendly simplified version of jwt-decode.js
function b64DecodeUnicode(str) {
return decodeURIComponent(
atob(str).replace(/(.)/g, function (m, p) {
var code = p.charCodeAt(0).toString(16).toUpperCase();
if (code.length < 2) {
code = "0" + code;
}
return "%" + code;
})
);
}
function base64_url_decode(str) {
var output = str.replace(/-/g, "+").replace(/_/g, "/");
switch (output.length % 4) {
case 0:
break;
case 2:
output += "==";
break;
case 3:
output += "=";
break;
default:
throw "Illegal base64url string!";
}
try {
return b64DecodeUnicode(output);
} catch (err) {
return atob(output);
}
}
export function jwtDecode(token, options) {
options = options || {};
var pos = options.header === true ? 0 : 1;
try {
return JSON.parse(base64_url_decode(token.split(".")[pos]));
} catch (e) {
console.log(e.message);
}
}

Related

PublicKeyCredential not possible to serialize

I am implementing FIDO2(WebAuthn) in a Angular application.
I have gotten the PublicKeyCredentialCreationOptions object and seccessfullt register.
But after calling
let response = await navigator.credentials.create({'publicKey': myPublicKeyCredentialCreationOption })
I try to send the response to the server.. But this fails.
When I tried to look at the object in the browser using
console.log(JSON.stringify(response))
I get
{}
as output (?..) but when doing
console.log(response)
I get a object with values in the console...
How should the object get serialized to send to the server?
PublicKeyCredential objects contains ArrayBuffer objects that cannot be serialized as JSON. You could base64 encode these values in your Angular app and decode on the server to get the same byte array back. A helper library to do exactly that for WebAuthn exists: https://github.com/github/webauthn-json
Here's a very simple example for anyone who needs it:
function bufferToBase64url (buffer) {
// modified from https://github.com/github/webauthn-json/blob/main/src/webauthn-json/base64url.ts
const byteView = new Uint8Array(buffer);
let str = "";
for (const charCode of byteView) {
str += String.fromCharCode(charCode);
}
// Binary string to base64
const base64String = btoa(str);
// Base64 to base64url
// We assume that the base64url string is well-formed.
const base64urlString = base64String.replace(/\+/g, "-").replace(
/\//g,
"_",
).replace(/=/g, "");
return base64urlString;
}
...
create publicKeyCredentialCreationOptions
...
navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
}).then(credential => {
// credential created
// console.log(credential); <-- check what is output to see what you need to call bufferToBase64url(credential.<...>) on down below
// convert credential to json serializeable
const serializeable = {
authenticatorAttachment: credential.authenticatorAttachment,
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
response: {
attestationObject: bufferToBase64url(credential.response.attestationObject),
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON)
},
type: credential.type
};
const serialized = JSON.stringify(serializeable);
console.log(serialized);
}).catch(err => {
// an error occurred
console.error(err);
});

How do I convert this JS signature generator function into Ruby?

I'am trying to integrate the Zoom SDK into my application and I am having trouble figuring out how to convert their example code for generating the signature into ruby.
Example Code -
const base64JS = require('js-base64');
const hmacSha256 = require('crypto-js/hmac-sha256');
const encBase64 = require('crypto-js/enc-base64');
function generateSignature(data) {
let signature = '';
const ts = new Date().getTime();
const msg = base64JS.Base64.encode(data.apiKey + data.meetingNumber + ts + data.role);
const hash = hmacSha256(msg, data.apiSecret);
signature = base64JS.Base64.encodeURI(`${data.apiKey}.${data.meetingNumber}.${ts}.${data.role}.${encBase64.stringify(hash)}`);
return signature;
}
const data = {apiKey: "" ,
apiSecret: "",
meetingNumber: 888,
role: 0}
console.log(generateSignature(data));
How would the generateSignature function look like in ruby?
I've tried a few times but the outputted signature differed when I tried writing this in Ruby. I suspect that I'am encoding and decoding improperly.
This is the javascript code above that I modified slightly to cross reference
const base64JS = require('js-base64');
const hmacSha256 = require('crypto-js/hmac-sha256');
const encBase64 = require('crypto-js/enc-base64');
function generateSignature(data) {
let signature = '';
const ts = "1569600658561"
const msg = base64JS.Base64.encode(data.apiKey + data.meetingNumber + ts + data.role);
console.log(msg); // This matches the ruby
const hash = hmacSha256(msg, data.apiSecret);
signature = base64JS.Base64.encodeURI(`${data.apiKey}.${data.meetingNumber}.${ts}.${data.role}.${encBase64.stringify(hash)}`);
return signature;
}
data = {
apiKey: 'api_key',
apiSecret: 'secret',
meetingNumber: '1000',
role: '0'
}
console.log(generateSignature(data));
This is my attempt in ruby
class ZoomSignatureGenerator
def self.generate
data = {
api_key: 'api_key',
api_secret: 'secret',
meeting_number: '1000',
role: '0'
}
ts = "1569600658561"
msg = Base64.encode64(data[:api_key] + data[:meeting_number] + ts + data[:role]);
puts(msg)
hash = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), data[:api_secret], msg)
signature = Base64.urlsafe_encode64("#{data[:api_key]}.#{data[:meeting_number]}.#{ts}.#{data[:role]}.#{Base64.encode64(hash)}");
return signature
end
end
I expected them to be the same output. But they end up being different.
Hope someone can help me :)
the output of Base64.encode64(value) is separated by a newline character, use Base64.strict_encode64(value) to generates the same result as Node.js.
require "base64"
require "openssl"
def generate_signature(api_key, api_secrete, meeting_number, role)
timestamp = 1544683367752
message = Base64.strict_encode64("#{api_key}#{meeting_number}#{timestamp}#{role}")
hash = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', api_secrete, message))
signature = Base64.strict_encode64("#{api_key}.#{meeting_number}.#{timestamp}.#{role}.#{hash}")
end
generate_signature("key", "secret", 123456, 0)
Encountered the same problem, apparently Zoom uses base 64 encoded binary string, so instead of using HMAC.hexdigest try this
hmac = OpenSSL::HMAC.new(API_KEY, 'sha256')
hmac << data
hash = Base64.encode64(hmac.digest)
digest method returns the authentication code as a binary string,
hexdigest returns the authentication code as a hex-encoded string

How to convert this signature method from crypto (node) to crypto-js (browser)?

I have a signature method that is meant to be used in Node.js but I'd like to implement it client-side with crypto-js. It should work in latest Chrome versions.
I have tried to follow some answers like this one: Decode a Base64 String using CryptoJS
But I either get errors such as "Error: Malformed UTF-8 data", or a different result than the expected hmacDigest.
I am not sure how I could find an alternative to the "binary" digest although I found this question:
How to get digest representation of CryptoJS.HmacSHA256 in JS
The method is supposed to answer the following:
"Message signature using HMAC-SHA512 of (URI path + SHA256(nonce + POST data)) and base64 decoded secret API key"
This is the Nodejs version (with crypto):
const crypto = require('crypto')
function sign(path, params, secret) {
const message = querystring.stringify(params)
const secretBase64 = Buffer.from(secret, 'base64')
const hash = crypto.createHash('sha256')
const hmac = crypto.createHmac('sha512', secretBase64)
const hashDigest = hash.update(params.nonce + message).digest('binary')
const hmacDigest = hmac.update(path + hashDigest, 'binary').digest('base64')
return hmacDigest
}
note: querystring is just an helper module that can also run in browsers: https://nodejs.org/api/querystring.html
This is my attempt (wip) at implementing with crypto-js:
import cryptojs from 'crypto-js')
function sign (path, params, secret) {
const message = querystring.stringify(params)
const secretParsed = cryptojs.enc.Base64.parse(secret)
const secretDecoded = cryptojs.enc.Utf8.stringify(secretParsed) // -> this throws an error as "Error: Malformed UTF-8 data"
const hash = cryptojs.SHA256(params.nonce + message).toString(cryptojs.enc.hex)
const hmac = cryptojs.HmacSHA512(path + hash, secretDecoded).toString(cryptojs.enc.hex)
return hmac
}
Try this ! I think this is what you looking for !
const crypto = require("crypto")
const sign = (path, params, secret) => {
const message = querystring.stringify(params)
const secret = Buffer.from(secret, 'base64')
let sign = params.nonce + message
let hash = crypto.createHmac('sha256', secret)
.update(sign)
.digest("base64").toString()
let encoded = encodeURIComponent(hash)
return encoded
}

Telegram API returning HTML instead of JSON

I'm writing a telegram bot to report fail2ban bans. It's very simple and dirty, written hastily, but it can be used to report any message to a single telegram user:
var TelegramBot = require('node-telegram-bot-api');
var fs = require('fs');
var store = {
get: function (key) {
return fs.readFileSync(__dirname + '/' + key, { encoding: 'utf-8' });
},
set: function (key, value) {
fs.writeFileSync(__dirname + '/' + key, value, { encoding: 'utf-8' });
}
};
var token = store.get('token');
var args = process.argv.slice(2);
if (args.length == 0) {
console.error('No mode specified');
process.exit(0);
}
TelegramBot.prototype.unregisterText = function (regexp) {
for (var i = 0; i < bot.textRegexpCallbacks.length; ++i) {
if (bot.textRegexpCallbacks[i].regexp.toString() == regexp) {
bot.textRegexpCallbacks.splice(i, 1);
return;
}
}
};
fs.appendFileSync(__dirname + '/logs',
'[' + (new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')) + '] '
+ args.join(' ') + '\n',
{ encoding: 'utf-8' });
switch (args[0]) {
case 'setup':
var bot = new TelegramBot(token, { polling: true });
var step = 'none';
bot.onText(/\/setup/, function (msg, match) {
var fromId = msg.from.id;
step = 'setup-started';
bot.sendMessage(fromId, 'Starting setup. Please enter the verification key.');
bot.onText(/(.+)/, function (msg, match) {
if (step == 'setup-started') {
var key = match[1];
var verification = store.get('key');
if (key == verification) {
store.set('owner', msg.from.id);
step = 'verified';
bot.sendMessage(msg.from.id, 'Correct. Setup complete.');
} else {
step = 'none';
bot.unregisterText(/(.+)/);
bot.sendMessage(msg.from.id, 'Wrong. Setup aborted.');
}
}
});
});
break;
case 'report':
var bot = new TelegramBot(token, { polling: false });
var owner = store.get('owner');
var subject = args[1];
if (subject == 'message') {
var message = args.slice(2).join(' ');
bot.sendMessage(owner, message);
} else if (subject == 'file') {
var content = fs.readFileSync(args[2], { encoding: 'utf-8' });
bot.sendMessage(owner, content);
}
break;
default:
console.error('Unrecognized mode', args[0]);
break;
}
On my developer machine it works fine. I invoke:
node bot.js report message whatever message i want
And I correctly received "whatever message i want" on telegram. However, once I gitted it on my digitalocean vps, it no longer worked. It turns out the problem is with the telegram library:
Unhandled rejection Error: Error parsing Telegram response: <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bots: An introduction for developers</title>
...
Which apparently returns an html page instead of json... I also tried to contact the same endpoint (api.telegram.org/bothash/sendMessage) with curl on my vps and it returned json (with an error message because i didnt send any parameters, but still json).
I cannot fathom why this happens. Any help?
It seems like either you don't have a file with token on your VPN or the token is incorrect.
You can check it by yourself:
When you make a request to api.telegram.org/{token}/sendMessage, and {token} is incorrect, it redirects you to this page, which responds with HTML you've mentioned in your question.
So you have to debug a behavior of your store.get and store.get functions along with files and tokens to make sure you are using a correct one.
Also, I'd recommend to run bot.getMe() before using any other Telegram API methods to ensure you specified a correct bot token.

Convert Node.JS code snippet to Javascript (Google Apps Script)

I would like to convert the following Node.JS code snippet to JavaScript in order to run it in Google Apps Script:
From: Node.JS
function getMessageSignature(path, request, nonce) {
var message = querystring.stringify(request);
var secret = new Buffer(config.secret, 'base64');
var hash = new crypto.createHash('sha256');
var hmac = new crypto.createHmac('sha512', secret);
var hash_digest = hash.update(nonce + message).digest('binary');
var hmac_digest = hmac.update(path + hash_digest, 'binary').digest('base64');
return hmac_digest;
}
This is the code I have tried so far (and many variations of it):
To: JavaScript / Google Apps Script
function getMessageSignature(url, request, nonce) {
// Message signature using HMAC-SHA512 of (URI path + SHA256(nonce + POST data))
//and base64 decoded secret API key
const secretApiKey = 'wdwdKswdKKewe23edeYIvL/GsltsGWbuBXnarcxZfu/9PjFbXl5npg==';
var secretApiKeyBytes = Utilities.base64Decode(secretApiKey);
var blob = Utilities.newBlob(secretApiKeyBytes);
var secretApiKeyString = blob.getDataAsString(); // POTENTIAL ERROR HERE?
var json = Utilities.jsonStringify(request);
var hash_digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,
nonce + json);
var hmac_digest = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_512,
url + hash_digest, secretApiKeyString); // POTENTIAL ERROR HERE?
var base64 = Utilities.base64Encode(hmac_digest);
return base64;
}
When sending the signature as part of my request to the server, I always get the error message from the server: Invalid Key.
BTW: This is the API which I would like to use in JavaScript: Kraken API
I would appreciate any hint or suggestions very much!!
Solution:
Use jsSHA (https://github.com/Caligatio/jsSHA/) rather than Google App Script's functions. Create a new "jsSHA.gs" code file in Google App Script and copy/past in all the jsSHA optimised .js files from github.
function getKrakenSignature (path, postdata, nonce) {
var sha256obj = new jsSHA ("SHA-256", "BYTES");
sha256obj.update (nonce + postdata);
var hash_digest = sha256obj.getHash ("BYTES");
var sha512obj = new jsSHA ("SHA-512", "BYTES");
sha512obj.setHMACKey (api_key_private, "B64");
sha512obj.update (path);
sha512obj.update (hash_digest);
return sha512obj.getHMAC ("B64");
}
function getKrakenBalance () {
var path = "/0/private/Balance";
var nonce = new Date () * 1000;
var postdata = "nonce=" + nonce;
var signature = getKrakenSignature (path, postdata, nonce);
var url = api_url + path;
var options = {
method: 'post',
headers: {
'API-Key': api_key_public,
'API-Sign': signature
},
payload: postdata
};
var response = UrlFetchApp.fetch (url, options);
// ERROR handling
return response.getContentText ();
}
One problem is that querystring.stringify is not the same as Utilities.jsonStringify (which, FYI, is deprecated in favor of JSON.stringify).
I believe that this will be equivalent:
function queryStringify(obj) {
var params = [];
for(var key in obj) {
if(Object.hasOwnProperty(key)) {
if(typeof key === 'string') {
params.push([key, obj[key]]);
} else {
obj[key].forEach(function(val) {
params.push([key, val]);
});
}
}
}
return params.map(function(param) {
return encodeURIComponent(param[0]) + '=' + encodeURIComponent(param[1]);
}).join('&');
}
Though I am not sure if that is the reason you are seeing your error.
I noticed this nodejs to GS converter: https://www.npmjs.com/package/codegs
Haven't got the chance to use it, but it claims to handle 'require'-statements.

Categories

Resources