June 13, 2026
HMAC Internals
How it works and why should you use it?
Pushpak Jalan
4 min read
Hey! Welcome to another of my blogs. Today I am presenting you something on security, more specifically, HMAC. We will be exploring it in depth and get to know when we should use it. So, let's not waste another moment in pleasantries and just get started.
CIA triad
One of the fundamental things to know about security is the CIA triad. It stands for confidentiality, integrity and availability.
- Confidentiality: Only the intended parties could see your data.
- Integrity: The data cannot be maliciously altered.
- Availability: Access to the data should not be denied to authorized personnel.
IAS octad further extends this by adding authentication, authorization, non-repudiation, accountability and privacy.
SHA-256 for integrity
It's in common knowledge that one-way hashes provide you with a way to check data integrity. SHA-256 is one such example. Now, SHA-256 is great but its susceptible to length extension attacks. What this means is that an attacker can append data to your message and since, the hash you provided with the original message represents the internal state of the hashing engine at the end of the algorithm, he or she can use it to initialize the hashing engine once again and use it to calculate the hash of the new message. This way the integrity check at the receiver passes while the message has been modified.
✍️ Enjoying this? I write across the modern engineering stack. Follow me here and you'll always have something worth reading.
Fast-forward to HMAC
HMAC stands for Hash-based Message Authentication Code. It elegantly solves what SHA-256 originally intends to solve (i.e., integrity) but also, takes it a step further by introducing authentication, essentially recognizing the sender of the message. It does so by introducing a shared secret key between the sender and receiver. This is how it works:
HMAC(K, M) = H( (K XOR opad) + H( (K XOR ipad) + M ) )
where,
- K is the secret key,
- H is a one-way hash function, for example, SHA-256
- M is the original message
- ipad is the inner padding, i.e., a fixed block of bytes made by repeating the hex value 0x36.
- opad is the outer padding, i.e., a fixed block of bytes made by repeating the hex value 0x5C.
The way it solves length extension attacks is by doing a dual hash. Plus, since a malicious actor won't have access to the shared key, it proves the message came from the actual sender. Note that HMAC doesn't solve confidentiality. The message is still sent from sender to receiver in plaintext. It just verifies the integrity and sender identity by recomputing the hash based on the message and shared secret and matching it with the one sent by the sender.
Implementing HMAC in NodeJS
// filename: server/main.js
const http = require("http");
const crypto = require("crypto");
const SECRET_KEY = "my-super-secret-key"; // Shared secret between client & server
const PORT = 3000;
/**
* Verifies the HMAC signature on an incoming request.
* Expects headers:
* x-timestamp — Unix timestamp (ms) when the request was signed
* x-signature — HMAC-SHA256 hex digest of "<timestamp>.<body>"
*/
function verifySignature(body, timestamp, receivedSignature) {
// Reject requests older than 5 minutes (replay-attack protection)
const now = Date.now();
const age = Math.abs(now - Number(timestamp));
if (age > 5 * 60 * 1000) {
return { valid: false, reason: "Request timestamp is too old" };
}
const payload = `${timestamp}.${body}`;
const expectedSignature = crypto
.createHmac("sha256", SECRET_KEY)
.update(payload)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
const expected = Buffer.from(expectedSignature, "hex");
const received = Buffer.from(receivedSignature, "hex");
if (
expected.length !== received.length ||
!crypto.timingSafeEqual(expected, received)
) {
return { valid: false, reason: "Signature mismatch" };
}
return { valid: true };
}
const server = http.createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/api/data") {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
return;
}
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const timestamp = req.headers["x-timestamp"];
const signature = req.headers["x-signature"];
if (!timestamp || !signature) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing x-timestamp or x-signature headers" }));
return;
}
const { valid, reason } = verifySignature(body, timestamp, signature);
if (!valid) {
console.error(`[Server] ❌ Auth failed: ${reason}`);
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized", reason }));
return;
}
console.log(`[Server] ✅ Signature verified. Body: ${body}`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", received: JSON.parse(body) }));
});
});
server.listen(PORT, () => {
console.log(`[Server] Listening on http://localhost:${PORT}`);
});
// filename: client/main.js
const http = require("http");
const crypto = require("crypto");
const SECRET_KEY = "my-super-secret-key"; // Must match the server's secret
const SERVER_HOST = "localhost";
const SERVER_PORT = 3000;
/**
* Signs a request body with HMAC-SHA256.
* Returns headers that should be attached to the HTTP request.
*/
function signRequest(body) {
const timestamp = Date.now().toString();
const payload = `${timestamp}.${body}`;
const signature = crypto
.createHmac("sha256", SECRET_KEY)
.update(payload)
.digest("hex");
return {
"x-timestamp": timestamp,
"x-signature": signature,
};
}
/**
* Sends a signed POST request to the server.
*/
function sendSignedRequest(data) {
const body = JSON.stringify(data);
const hmacHeaders = signRequest(body);
const options = {
hostname: SERVER_HOST,
port: SERVER_PORT,
path: "/api/data",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
...hmacHeaders,
},
};
console.log(`[Client] Sending request with timestamp: ${hmacHeaders["x-timestamp"]}`);
console.log(`[Client] HMAC signature: ${hmacHeaders["x-signature"]}`);
const req = http.request(options, (res) => {
let response = "";
res.on("data", (chunk) => (response += chunk));
res.on("end", () => {
console.log(`[Client] Response status: ${res.statusCode}`);
console.log(`[Client] Response body:`, JSON.parse(response));
});
});
req.on("error", (err) => {
console.error(`[Client] Request error: ${err.message}`);
});
req.write(body);
req.end();
}
// --- Demo ---
// Valid request
sendSignedRequest({ message: "Hello, secure world!", userId: 42 });
// Tampered request (wrong signature) — demonstrates rejection
setTimeout(() => {
console.log("\n[Client] Sending tampered request...");
const body = JSON.stringify({ message: "Tampered payload" });
const options = {
hostname: SERVER_HOST,
port: SERVER_PORT,
path: "/api/data",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
"x-timestamp": Date.now().toString(),
"x-signature": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
},
};
const req = http.request(options, (res) => {
let response = "";
res.on("data", (chunk) => (response += chunk));
res.on("end", () => {
console.log(`[Client] Tampered response status: ${res.statusCode}`);
console.log(`[Client] Tampered response body:`, JSON.parse(response));
});
});
req.write(body);
req.end();
}, 500);// filename: server/main.js
const http = require("http");
const crypto = require("crypto");
const SECRET_KEY = "my-super-secret-key"; // Shared secret between client & server
const PORT = 3000;
/**
* Verifies the HMAC signature on an incoming request.
* Expects headers:
* x-timestamp — Unix timestamp (ms) when the request was signed
* x-signature — HMAC-SHA256 hex digest of "<timestamp>.<body>"
*/
function verifySignature(body, timestamp, receivedSignature) {
// Reject requests older than 5 minutes (replay-attack protection)
const now = Date.now();
const age = Math.abs(now - Number(timestamp));
if (age > 5 * 60 * 1000) {
return { valid: false, reason: "Request timestamp is too old" };
}
const payload = `${timestamp}.${body}`;
const expectedSignature = crypto
.createHmac("sha256", SECRET_KEY)
.update(payload)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
const expected = Buffer.from(expectedSignature, "hex");
const received = Buffer.from(receivedSignature, "hex");
if (
expected.length !== received.length ||
!crypto.timingSafeEqual(expected, received)
) {
return { valid: false, reason: "Signature mismatch" };
}
return { valid: true };
}
const server = http.createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/api/data") {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
return;
}
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const timestamp = req.headers["x-timestamp"];
const signature = req.headers["x-signature"];
if (!timestamp || !signature) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing x-timestamp or x-signature headers" }));
return;
}
const { valid, reason } = verifySignature(body, timestamp, signature);
if (!valid) {
console.error(`[Server] ❌ Auth failed: ${reason}`);
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized", reason }));
return;
}
console.log(`[Server] ✅ Signature verified. Body: ${body}`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", received: JSON.parse(body) }));
});
});
server.listen(PORT, () => {
console.log(`[Server] Listening on http://localhost:${PORT}`);
});
// filename: client/main.js
const http = require("http");
const crypto = require("crypto");
const SECRET_KEY = "my-super-secret-key"; // Must match the server's secret
const SERVER_HOST = "localhost";
const SERVER_PORT = 3000;
/**
* Signs a request body with HMAC-SHA256.
* Returns headers that should be attached to the HTTP request.
*/
function signRequest(body) {
const timestamp = Date.now().toString();
const payload = `${timestamp}.${body}`;
const signature = crypto
.createHmac("sha256", SECRET_KEY)
.update(payload)
.digest("hex");
return {
"x-timestamp": timestamp,
"x-signature": signature,
};
}
/**
* Sends a signed POST request to the server.
*/
function sendSignedRequest(data) {
const body = JSON.stringify(data);
const hmacHeaders = signRequest(body);
const options = {
hostname: SERVER_HOST,
port: SERVER_PORT,
path: "/api/data",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
...hmacHeaders,
},
};
console.log(`[Client] Sending request with timestamp: ${hmacHeaders["x-timestamp"]}`);
console.log(`[Client] HMAC signature: ${hmacHeaders["x-signature"]}`);
const req = http.request(options, (res) => {
let response = "";
res.on("data", (chunk) => (response += chunk));
res.on("end", () => {
console.log(`[Client] Response status: ${res.statusCode}`);
console.log(`[Client] Response body:`, JSON.parse(response));
});
});
req.on("error", (err) => {
console.error(`[Client] Request error: ${err.message}`);
});
req.write(body);
req.end();
}
// --- Demo ---
// Valid request
sendSignedRequest({ message: "Hello, secure world!", userId: 42 });
// Tampered request (wrong signature) — demonstrates rejection
setTimeout(() => {
console.log("\n[Client] Sending tampered request...");
const body = JSON.stringify({ message: "Tampered payload" });
const options = {
hostname: SERVER_HOST,
port: SERVER_PORT,
path: "/api/data",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
"x-timestamp": Date.now().toString(),
"x-signature": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
},
};
const req = http.request(options, (res) => {
let response = "";
res.on("data", (chunk) => (response += chunk));
res.on("end", () => {
console.log(`[Client] Tampered response status: ${res.statusCode}`);
console.log(`[Client] Tampered response body:`, JSON.parse(response));
});
});
req.write(body);
req.end();
}, 500);Parting thoughts
Understanding security mechanisms is crucial to implementing secure systems. In an AI era, it becomes much more important to spend time in learning this topic in depth. HMAC is just one tool in the box to ensure this, but you should go beyond to explore others in depth.
If you found this article helpful or inspiring, feel free to give it a clap 👏 — it really helps keep me motivated to create more content like this.
Don't forget to follow me for more insightful posts, and I'd genuinely love to hear your thoughts. Drop a comment below and let me know what you build with this — I'm excited to see your ideas come to life!
See you in the next article — until then, happy building! 🚀