How can I verify a HMAC SHA256 in JavaScript?

Viewed 2662

I'm writing a script in Node.js that integrates Todoist, Notion and Discord. I'm using Heroku for hosting because I want to use webhooks (from Todoist). The idea is that every time I add or complete a task, a notification is fired by Todoist to my app and after verifying that the request is legitimate, I update my Notion database and send messages through a discord bot.

The problem I encounter is when I have to check the HMAC SHA256. I'm still quite new to JavaScript and even newer to hashing and secret keys, so please bear with me.

The Todoist documentation says:

To verify each webhook request was indeed sent by Todoist, an X-Todoist-Hmac-SHA256 header is included; it is a SHA256 Hmac generated using your client_secret as the encryption key and the whole request payload as the message to be encrypted. The resulting Hmac would be encoded in a base64 string.

After a lot of research, I tried to use a function I found as the verified answer in another question, but this happens: throw new Error('Malformed UTF-8 data');

This is what I have so far:


const express = require('express');
const Discord = require('discord.js');
const client = new Discord.Client();
var CryptoJS = require('crypto-js');
require('dotenv').config();

const PORT = process.env.PORT || 3000;

app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

process.on('unhandledRejection', error => {
    // Will print "unhandledRejection err is not defined"
    console.log('unhandledRejection', error.message);
});

function sign_string(message, key){
    var secret_key = CryptoJS.enc.Base64.parse(key).toString(CryptoJS.enc.Utf8);
    var hash = CryptoJS.HmacSHA256(message, secret_key);
    return CryptoJS.enc.Base64.stringify(hash);
}

client.on('ready', () => {
    client.users.fetch(process.env.MY_USER_ID).then(user => user.send('Hey, The bot is up!'));
});

app.get('', (req, res) => {
    res.send('Hello World');
});

app.post('', (req, res) => {
    if(req.get('User-Agent') === 'Todoist-Webhooks') {
        var delivered_hmac = req.get('X-Todoist-Hmac-SHA256');
        var computed_hmac = sign_string(JSON.stringify(req.body), process.env.TODOIST_CLIENT_SECRET);
        if(delivered_hmac === computed_hmac) {
            if(req.body.event_name === 'item:added' && req.body.event_data.description === '') {
                // add task to notion
                // idea: ask user for data
            } else {
                if(req.body.event_name === 'item:completed' && req.body.event_data.description !== '') {
                    // complete task on notion
                }
            }
            client.users.fetch(process.env.MY_USER_ID).then(user => user.send('You can update your tasklist if you want'));
            res.status(200).send('Event handled');
        } else {
            client.users.fetch(process.env.MY_USER_ID).then(user => user.send('A 403 (Unauthorized) status code has been sent to a request'));
            res.status(403).send('Unauthorized');
            console.log(`delivered_hmac: ${delivered_hmac}\ncomputed_hmac: ${computed_hmac}\n`)
            console.log(req.body);
        }
    } else {
        client.users.fetch(process.env.MY_USER_ID).then(user => user.send('A 400 (Bad request) status code has been sent to a request'));
        res.status(400).send('Bad request');
    }
    //handle notion


})


client.login(process.env.BOT_TOKEN);
app.listen(PORT, () => {
    console.log(`App up at port ${PORT}`);
});
0 Answers
Related