HMAC Validation
In this HMAC Validation tutorial, we will define what HMAC is, its benefits, and implement HMAC validation as a middleware in an API project.
HMAC stands for hash-based message authentication code. An HMAC algorithm works by hashing a message along with a secret key. The resulting hash is called a signature or digest. The message is then transferred to a recipient along with the signature. If the recipient has the secret key, they can hash the message with the same algorithm and verify the resulting signature matches the one sent with the message. As a result, the message is simultaneously authenticated and its integrity verified.
The two main benefits of HMAC include the following.
- First, since an HMAC requires a secret key, both the sender and recipient have to have the same secret key for HMAC verification to work. Unlike access token, the secret key is never sent with the message, therefore it can’t be intercepted. As a result, the HMAC acts similarly to a password. It is very important to keep the secret key safe. Since it is used like a password, it needs to be saved in encrypted storage. It is also important to use a sufficiently long secret key that cannot be brute forced by password breakers.
- Second, an HMAC is based not only on the secret key, but also on the message itself. If two HMACs are created with the same secret key, but different messages, the resulting codes will be different. When the API sees that its HMAC matches the HMAC in the request, it knows the request was not modified during transport. Since hackers will try to modify messages in between clients and servers, HMAC signatures can be a powerful tool to verify authenticity of requests.
HMAC Validation Implementation
For a starting point of this tutorial please clone https://github.com/alexrusin/node-api-starter Please watch the following video for more in depth explanation:
In order to calculate HMAC, we need to get hold of raw body of the request. The easiest way to do it in Express is to use verify function in express.json
middleware in server.ts
file.
app
.disable("x-powered-by")
.use(morgan("dev"))
.use(express.urlencoded({ extended: true }))
.use(
express.json({
verify: (req: Request, res, buf) => {
if (req.originalUrl === EnvManager.getWebhooksRoute()) {
req.rawBody = buf;
}
},
})
)
.use(cors());
However, rawBody property doesn’t exist on Express request. We need to add it to Express request interface. In src
folder let’s create express.d.ts
file with the following content:
import "express";
declare global {
namespace Express {
interface Request {
rawBody?: Buffer;
}
}
}
Since interface declarations get merged. we essentially extended Express request interface by adding rawBody
property. We also have to be sure express.d.ts
file is included into types option of tsconfig.json
if you have types option explicitly defined. You can read this article more information about TypeScript interfaces and module augmentation, please read this article.
Now let’s add functions to create and validate HMAC to functions.ts
file in the utils
folder.
export function createHmac(message: string) {
const hmac = crypto.createHmac("sha256", EnvManager.getAppSecret());
hmac.update(message);
return hmac.digest("hex");
}
export function verifyHmac(message: string, receivedHmac: string) {
const hmacFromMessage = createHmac(message);
return crypto.timingSafeEqual(
Buffer.from(hmacFromMessage, "utf8"),
Buffer.from(receivedHmac, "utf8")
);
}
We are using SHA256 HMAC and digest it to hex. Function verifyHmac is using timingSafeEqual function to compare received HMAC digest and HMAC digest created form the message.
Now let’s create validateHmac
middleware function in the middleware
folder that will validate the HMAC.
import { NextFunction, Request, Response } from "express";
import logger from "../utils/Logger";
import { verifyHmac } from "../utils/functions";
export default function validateHmac(
req: Request,
res: Response,
next: NextFunction
) {
const hmacSignature = req.get("X-HMAC");
if (!hmacSignature) {
throw new Error("HMAC signature is missing");
}
const bodyString = req.rawBody?.toString() || "";
try {
if (!verifyHmac(bodyString, hmacSignature)) {
throw new Error("HMAC is invalid");
}
} catch (error) {
logger.error`Hmac validation error ${error}`;
throw new Error("HMAC is invalid");
}
next();
}
Since we use errorHandler
we can just throw an error from the middleware and it will be correctly returned as a 400 error response.
Finally, let’s attach middleware to the /api/webhooks
route.
import express, { Router } from "express";
import webhooks from "./webhooks";
import validateHmac from "../../middleware/validateHmac";
const api: Router = express.Router();
api.use("/webhooks", validateHmac, webhooks);
export default api;
In order to test the code, we need to generate HMAC from message on the client side with the same algorithm (SHA256) and the same secret key. We can do it in Postman using pre request script.
// Postman pre request script
var signature = CryptoJS.HmacSHA256(pm.request.body.raw, '240e25ca6912afecf2e5b9c113b6c05686ad05f9213c30ce0ec331df15d218cb').toString();
pm.request.headers.add({
key: "X-HMAC",
value: signature
});
Thy sending a post request with JSON
payload to /api/webhooks
endpoint. The validation should pass now.
Conclusion
In conclusion, HMAC validation stands as a formidable guardian of data integrity and authenticity in the realm of cybersecurity. As we’ve delved into its intricacies, it’s evident that this cryptographic technique can insure that data remains untampered and trustworthy during transmission and storage. Its adaptability and wide-ranging applications make it an invaluable tool for safeguarding sensitive information across various domains, from web security to API authentication.
Resources
Node.js API Starter Repository
TypeScript Module Augmentation Section of Interfaces Article on Digital Ocean