Link Search Menu Expand Document

Setup HMAC Authentication in Node.js

We will be using Node.js version 16 (v16.13.0) and npm version 8 (v8.1.0). We assume you are already familiar with Node.js and have npm installed on your system. Let’s get started.

Install Dependencies

Because we’ll be using npm, make sure you have the package.json file in your working directory. If you don’t have the file, make one by running this command npm init. Then you will be asked couple question regarding your npm project and once it done the content of the file will look like this:

{
  "name": "your-node-project",
  "version": "0.1.0",
  "description": "Your project description",
  "main": "main.js",
  "author": "Your Name <your-email@example.com>",
  "dependencies": {}
}

There will be two dependency: Axios to make HTTP requests to the Mekari API and dotenv for environment variable loader, because we don’t want to put the Mekari API client ID and client secret directly on the code.

$ npm add axios dotenv --save

Once installed, your dependencies attribute on the package.json will look something like this.

{
  "dependencies": {
    "axios": "^0.24.0",
    "dotenv": "^10.0.0"
  }
}

Making API Request

Using Axios that we have installed earlier, we are going to setup a script that will perform API request to one of Klikpajak API endpoint which is create sales invoice (https://api.mekari.com/v2/klikpajak/v1/efaktur/out) with query param ?auto_approval=false.

We create the script called main.js then use Axios to perform the HTTP POST request. This is how the script will looks like:

const axios = require('axios');

axios({
  method: 'POST',
  url: 'https://api.mekari.com/v2/klikpajak/v1/efaktur/out?auto_approval=false'
}).then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    console.log(error);
  });

We can run the script on the console by typing node main.js. Because we just created a plain HTTP POST request, it will throw an error similar to this:

$ node main.js
{
  {
    // ...a lot of data
    data: { message: 'Unauthorized' }
  },
  isAxiosError: true,
  toJSON: [Function: toJSON]
}

Before we add authentication to the request, let’s catch the error and display it nicely on the console.

const axios = require('axios');

axios({
  method: 'POST',
  url: 'https://api.mekari.com/v2/klikpajak/v1/efaktur/out?auto_approval=false'
}).then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in node.js
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
  });

When we run the script again, the error message will be as follows:

$ node main.js
{ message: 'Unauthorized' }
401
{
  date: 'Thu, 25 Nov 2021 02:21:08 GMT',
  'content-type': 'application/json; charset=utf-8',
  'content-length': '26',
  'x-envoy-upstream-service-time': '2',
  server: 'Mekari',
  connection: 'close'
}

Creating HMAC Signature

The signature is one of the requirements for forming an API request with HMAC Authentication. The signature is an HMAC256 representation of the request line (a combination of the request method, the request path, the query param and HTTP/1.1) and the Date header in RFC 7231 format. The signature must then be converted into a Base64 string so that it can be attached to the Authorization header.

The code will look like this:

// ... the rest of the code

const datetime = new Date().toUTCString();
const requestLine = "POST /v2/klikpajak/v1/efaktur/out?auto_approval=false  HTTP/1.1";
const payload = [`date: ${datetime}`, requestLine].join('\n');
const signature = crypto.createHmac('SHA256', 'YOUR_MEKARI_CLIENT_SECRET').update(payload).digest('base64');

console.log(`hmac username="YOUR_MEKARI_CLIENT_ID", algorithm="hmac-sha256", headers="date request-line", signature="${signature}"`)

If you replace datetime with Wed, 10 Nov 2021 07:24:29 GMT and run the code, you will get the following result.

$ node main.js
hmac username="YOUR_MEKARI_CLIENT_SECRET", algorithm="hmac-sha256", headers="date request-line", signature="6+Ah/UTmbqd+DDqlh6zYZ0HuCwVhtElYDOoucRPCaFg="

It is important to note that we should not include any credentials in our codebase. This means that we must save the Mekari API client id and client secret that you obtained from the Mekari Developer dashboard to an environment variable. Modern full-stack frameworks, such as Laravel, usually include an .env file to make managing environment variables easier. This is also why phpdotenv was installed. We can use this library to move the client id and client secret to the .env file.

# .env file
MEKARI_API_BASE_URL=https://api.mekari.com
MEKARI_API_CLIENT_ID=YOUR_MEKARI_API_CLIENT_ID
MEKARI_API_CLIENT_SECRET=YOUR_MEKARI_CLIENT_SECRET

Then we use process.env.* to replace the credentials in the code.

const axios = require('axios');
const crypto = require('crypto');

require('dotenv').config();

// ... the rest of the code

const datetime = new Date().toUTCString();
const requestLine = "POST /v2/klikpajak/v1/efaktur/out?auto_approval=false  HTTP/1.1";
const payload = [`date: ${datetime}`, requestLine].join('\n');
const signature = crypto.createHmac('SHA256', process.env.MEKARI_API_CLIENT_SECRET).update(payload).digest('base64');

console.log(`hmac username="${process.env.MEKARI_API_CLIENT_ID}", algorithm="hmac-sha256", headers="date request-line", signature="${signature}"`)

Wrap It Out

It’s now time to combine everything we’ve learned into a single Node.js script.

const axios = require('axios');
const crypto = require('crypto');

require('dotenv').config();

/**
 * Generate authentication headers based on method and path
 */
function generate_headers(method, pathWithQueryParam) {
  let datetime = new Date().toUTCString();
  let requestLine = `${method} ${pathWithQueryParam} HTTP/1.1`;
  let payload = [`date: ${datetime}`, requestLine].join("\n");
  let signature = crypto.createHmac('SHA256', process.env.MEKARI_API_CLIENT_SECRET).update(payload).digest('base64');

  return {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Date': datetime,
    'Authorization': `hmac username="${process.env.MEKARI_API_CLIENT_ID}", algorithm="hmac-sha256", headers="date request-line", signature="${signature}"`
  };
}

// Set method and path for the request
let method = 'POST';
let path = '/v2/klikpajak/v1/efaktur/out';
let queryParam = '?auto_approval=false';
let headers = {
  'X-Idempotency-Key': '1234'
};
let body = {/* request body */};

const options = {
  method: method,
  url: `${process.env.MEKARI_API_BASE_URL}${path}${queryParam}`,
  headers: {...generate_headers(method, path + queryParam), ...headers}
}

// Initiate request
axios(options)
  .then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in node.js
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
  });

If you want to integrate this script with your existing code, you may need to modify it. Additionally, each Mekari API has its own requirements regarding request headers, query, or body, and you may need to change the script based on your needs. We hope that this guide makes integrating Mekari API into your code easier for you.

You can also look at the final code on this repository.