DEV Community

Burhanuddin Ahmed
Burhanuddin Ahmed

Posted on

The Right Way to Do HMAC Authentication in ExpressJS

Authentication is the most important thing when building an API. There are many method to authenticate to an API. Some provider choose to only bearer token, some choose to use a pair of username and password, and many other way to authenticate an API. Besides those, there is another way to authenticate an API, yes, it is using an hmac comparison.

Usually an API webhook use this kind of authentication.

hmac authentication system

A webhook will be sent by third party system to invoke an API in our system when a certain action happened. In example, when a payment just done by customers or an order just created, etc. We only need to provide a POST API that will do some processes when there is an invocation. What we need to do is providing an API then usually in the third party system there is a configuration to set the endpoint to hit when an action is triggered.

example: payment just done then send payment status notification to our system via webhook, so we can change the status in our DB.

One of the popular authentication method is using HMAC hash, where third party system will send a hash code in the header and our API will authenticate from the hash code by generating HMAC then compare the HMAC hash code result with the hash code sent by partner.

The idea of this authentication is to make sure that the only party can invoke our webhook API is only the particular partner we have been configured, so not everyone can hit our API.

Example We want only Stripe that can invoke the webhook to send payment status. We dont want some random guy hitting and spamming the webhook using Postman and send random data.

At this point, we should follow the authentication method that is provided by the third party system.

Generate the Hmac

The most common way to generate a hmac is that the third party system will give us a secret code to generate hmac and then use the request body as encoding string (Buffer is also possible). Usually partner will have a documentation about how they generate the hmac hash and we just need to follow the logic using our prefered programming language.

func generateHMAC(message string, secretKey string) (string, error) {
    key := []byte(secretKey)
    h := hmac.New(sha256.New, key)

    _, err := h.Write([]byte(message))
    if err !=  nil {
        return "", err
    }

    // Get the HMAC value
    signature := h.Sum(nil)

    // Encode the signature in hexadecimal format
    encodedSignature := hex.EncodeToString(signature)

    return encodedSignature, nil
}
Enter fullscreen mode Exit fullscreen mode

or in Javascript

function generateHmac(
  message: string,
  secretKey: string,
  options?: HmacOptions
): string {
  const algorithm = options?.algorithm || 'sha256';
  const encoding = options?.encoding || 'hex';

  const hmac = crypto.createHmac(algorithm, secretKey);
  hmac.update(message);
  const signature = hmac.digest(encoding);

  return signature;
}
Enter fullscreen mode Exit fullscreen mode

Above just a simpel way to generate an HMAC hash.

How to do it the right way

As the title said, there is nothing wrong about the function to generate HMAC. The problem is in the argument that were sent to the function. I have this use case when using ExpressJS. Are we supposed to send the correct message to generateHmac() or not?

Usually we will use this method, pass req.body in the argument. Basically it will work fine until the partner send an encoded character, it will start failing.

import express from 'express';

const app: express.Application = express();

// Configure body-parser to handle JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// Define a simple route that responds with a message
app.get('/', (req: express.Request, res: express.Response) => {
  const hash = generateHmac(JSON.stringify(req.body), process.env.SECRET_CODE);
  if (hash !== req.headers.hash) { // throw an error here }

  res.send('Hello from a simple Express.js app!');
});
Enter fullscreen mode Exit fullscreen mode

express.json() will automatically decode the character into a readable one. Thats why the message argument in our end and in partner end to generate the HMAC would be different.

I give you an example. The partner send the message in the payload Hi, Tom \u0026 Jerry, bodyParser will translate it as Hi, Tom & Jerry and we will generate HMAC using the translated string, which will generate different HMAC code to partner end.

Solution

Using raw body will always be the workaround. We can get raw data from express.json(), then append it into the request object.

app.use(
  express.json({
    verify(req: express.Request, res, buf, encoding) {
      req.rawBuffer = buf;
    }
  })
);

. . . 
. . .

app.get('/', (req: express.Request, res: express.Response) => {
  const hash = generateHmac(req.rawBuffer, process.env.SECRET_CODE);
  if (hash !== req.headers.hash) { // throw an error here }

  res.send('Hello from a simple Express.js app!');
});
Enter fullscreen mode Exit fullscreen mode

Make sure generateHmac() parameters should accept Buffer too.

By using raw data, we can make sure that the message we use to generate HMAC is same between partner end and our end, there is no data discrepancy.

Top comments (0)