Securing API using Request Signing – Implementing HMAC in NodeJs
When we want to consume a message from a third-party system, we want to make sure that the message being received by the recipient is not modified in flight and it is coming from the right source, i.e. the right producer. HMAC is something that comes in handy in such scenarios. Later in the blog will have a look at how we can implement HMAC in NodeJs, but let’s begin with the HMACs intro.
Intro
HMAC stands for Hash-Based Message Authentication Code which is basically a type of message authentication code that involves a cryptographic hash function and a secret cryptographic key. Using this we can verify data integrity and authentication of a message.
How does HMAC work? Basics of request signing
Consider the following system: Thomas is our “server” and listens for requests. Andrew is our real user, who wants to send a message to Thomas. Peter is the attacker that want to hack and manipulate the message. Andrew tries to identify himself to Thomas, by sending his name in the request, but if Peter is able to see Andrew’s message over the network or otherwise gain access to his “name” field, he can pretend to be him and send false messages to Thomas:
This is a common problem that we face when building web APIs – since users’ IDs/usernames are relatively well known, we cannot use these as identifiers. To combat this, we need to implement a MAC (Message Authentication Code), an algorithm that confirms that a given message came from its sender and that the data in the message hasn’t been altered in transit.
How does HMAC solve the problem here?
HMACs provides client and server with a shared private key that is known only to them. The client makes a unique hash (HMAC) for every request. When the client requests the server, it hashes the requested data with a private key and sends it as a part of the request. Both the message and key are hashed in separate steps making it secure. When the server receives the request, it makes its own HMAC. Both the HMACs are compared and if both are equal, the client is considered legitimate.
The formula for HMAC:
HMAC = hashFunc(secret key + message)
Implementing HMAC in NodeJs?
To implement HMAC in NodeJs we will create 3 express applications:
- “server” where we want to listen for the message.
- “good-client” will be our second application that will have a correct secret key as of “server” which will be used for hashing of the message we will exchange.
- “bad-client” will be a similar application to the “good-client” but this will have an invalid secret key for hashing and hence our “server” will reject the messages being sent to it by the “bad-client”.
To validate the message integrity we will look for the custom header named “x-signature” and the content of the body being passed to “server” and for this, we will try to calculate the HASH using our secret key and the body content and try to match the final result being passed to use in the custom header named “x-signature”. Here is what “server” code looks like in the express application:
// server application
const crypto = require('crypto')
const express = require('express')
const app = express()
app.use(express.json())
const port = 3000
// our secret key that is already shared with "good-client"
const SECRET_KEY = 'ABC123DEF456'
app.post('/', (req, res) => {
if(req.headers && req.headers['x-signature']){
const hash = crypto.createHmac('sha256',SECRET_KEY).update(JSON.stringify(req.body))
if(hash.digest('hex') == req.headers['x-signature'])
return res.json({ "success": true })
res.sendStatus(400)
}
else{
res.sendStatus(400)
}
})
app.listen(port, () => {
console.log(`Simple app listening on port ${port}`)
})
Now let’s have a look at the “good-client” where we have a correct secret key and we expect the “server” to respond to us with a 200 status code:
// good client
const crypto = require('crypto')
const express = require('express')
const app = express()
const port = 3001
// secret key same as of "server"
const SECRET_KEY = 'ABC123DEF456'
const axios = require('axios');
app.get('/', async (req, res) => {
const dataToSend = {
"id": 1,
"firstName": "Malkit"
}
const hash = crypto.createHmac('sha256', SECRET_KEY).update(JSON.stringify(dataToSend))
const sig = hash.digest('hex');
let resp = await axios.post("http://localhost:3000/", dataToSend, { headers: { "x-signature": sig } });
console.log(resp.data);
res.send("request sent!")
})
app.listen(port, () => {
console.log(`Simple app listening on port ${port}`)
})
“bad-client” have a similar code but as mentioned earlier we will intentionally put the wrong secret key to generate the hashing and as a result of this, we will be getting the wrong hash at the server end, and hence we can know that either the data is modified in flight or the secret key is wrong.
Source code of the example applications can be downloaded from here
Author: Malkit Singh
Techie who runs daily (almost) and loves beer, bikes and JavaScript.