Asana API Webhooks - verifying X-Hook-Signature in Node & TS

I would like to verify incoming requests from Asana on my server. According to the docs the X-Hook-Signature header is computed over the request body using SHA256 HMAC.

If I try to create the signature using the node built-in crypto library, the resulting hashes don’t match up with the header.

The documentation sadly didn’t specify how exactly the body has to be passed to the function. I tried passing it as string, JSON.stringyfied, Buffer etc. Nothing seems to generate matching signatures.

Currently the sever uses express.json() as middleware, but I also tried using express.raw() & express.text(). I also tried setting up my own middleware that added a req.rawBody property to hold the request body as buffer.

Would be nice if someone could help me out. :slight_smile:

Minimal exmaple in TS:

1 Like

Hello,

I have the same exact problem.
I follow the guideline available here Asana
The source code used in the video is available here devrel-examples/index.js at master · Asana/devrel-examples · GitHub

I followed the security protocole presented in the video.
I correctly get and save the secret present in the x-hook-secret header during the webhook creation.

My webhook seems to work correctly as my server receive the webhook events.

However during the security check the value of the computedSignature and headers[“x-hook-signature”] are not the same, so the check fail…

Here is an extract of the source code

if (req.headers["x-hook-signature"]) {
    const computedSignature = crypto
      .createHmac("SHA256", secret)
      .update(JSON.stringify(req.body))
      .digest("hex");
    if (
      !crypto.timingSafeEqual(
        Buffer.from(req.headers["x-hook-signature"]),
        Buffer.from(computedSignature)
      )
    ) {
      // Fail
      res.sendStatus(401);
    } else {
      // Success
      res.sendStatus(200);
      console.log(`Events on ${Date()}:`);
      console.log(req.body.events);
    }
  }

Instead of req.body, use req.body.data

Hi Phil,

Thanks for taking the time to answer.
Actually my body look like this:

body = {
    "events": [
        {
            "user": {
                "gid": "123456789",
                "resource_type": "user"
            },
            "created_at": "2022-07-27T14:19:19.062Z",
            "action": "added",
            "resource": {
                "gid": "123456789",
                "resource_type": "story",
                "resource_subtype": "section_changed"
            },
            "parent": {
                "gid": "123456789",
                "resource_type": "task",
                "resource_subtype": "default_task"
            }
        }
    ]
}

So there is no “data” field inside.
Note That I am using an AWS Lambda function instead of a regular webserver
Thanks,

Ah, right. And your code does match the Github sample you linked to… I’m afraid I don’t have any other ideas as to why it’s not working.

My code looks like this

const crypto = require("crypto");
const AWS = require('aws-sdk');

exports.handler = async (event, context) => {
    const ssm = new AWS.SSM();
    const name = "name_path_to_secret";
    const path = event.rawPath;
    const headers = event.headers;
    const body = event.body;
    let response;
    
    if(path == "/receiveWebhook") {
      if (headers["x-hook-secret"] != undefined) {
        console.log("This is a new webhook");
        const secret = headers["x-hook-secret"];
        const  parameter = await ssm.putParameter({
          Name: name,
          Value: secret,
          KeyId: "my_key_id",
          Overwrite: true,
          Type: "SecureString"
        }).promise();
        
        response = {
          statusCode: 200,
          headers: {
            "X-Hook-Secret":secret,
          },
          body: null,
        }
      } else if (headers["x-hook-signature"] != undefined) {
        console.log("Webhook check");
            
        const parameter = await ssm.getParameter({ 
            Name: name, 
            WithDecryption: true 
        }).promise();
        const secret = parameter.Parameter.Value;
        
        const computedSignature = crypto
          .createHmac("SHA256", secret)
          .update(body)
          .digest("hex");
        if (
          !crypto.timingSafeEqual(
            Buffer.from(headers["x-hook-signature"]),
            Buffer.from(computedSignature)
          )
        ) {
          // Fail
          response = {
            statusCode: 401,
            body:null
          }
          console.log("Fail");
        } else {
          // Success
          response = {
            statusCode: 200,
            body:null
          }
          console.log("Success",event);
        }
      } else {
        console.error("Something went wrong!");
      }
    } 

    return response;
};