X Xerobit

HMAC Authentication — Signing API Requests with Secret Keys

HMAC (Hash-based Message Authentication Code) signs API requests with a shared secret. Learn how HMAC-SHA256 works, how to implement request signing in Node.js and Python, and...

Mian Ali Khalid · · 5 min read
Use the tool
Hash Generator
Generate MD5, SHA-1, SHA-256, and SHA-512 hashes client-side.
Open Hash Generator →

HMAC signs a message with a secret key to prove both authenticity (sender knows the key) and integrity (message wasn’t modified). It’s the mechanism behind AWS Signature Version 4, webhook verification, and many API authentication schemes.

Use the Hash Generator to generate HMAC values for testing.

How HMAC works

HMAC(key, message) = Hash(key_outer || Hash(key_inner || message))

Where:
- Hash = SHA-256 (or SHA-1, SHA-512)
- key_inner = key XOR 0x36 (ipad)
- key_outer = key XOR 0x5C (opad)
- || = concatenation

This double-hash construction prevents length-extension attacks that would affect plain SHA-256 on hash(key + message).

Node.js HMAC

import crypto from 'crypto';

// Create HMAC:
function createHmac(message, secret, algorithm = 'sha256') {
  return crypto.createHmac(algorithm, secret)
    .update(message)
    .digest('hex');
}

// Verify HMAC (constant-time comparison to prevent timing attacks):
function verifyHmac(message, secret, expected) {
  const computed = createHmac(message, secret);
  const computedBuf = Buffer.from(computed, 'hex');
  const expectedBuf = Buffer.from(expected, 'hex');
  
  if (computedBuf.length !== expectedBuf.length) return false;
  
  // timingSafeEqual prevents timing attacks:
  return crypto.timingSafeEqual(computedBuf, expectedBuf);
}

// Example:
const secret = 'my-super-secret-key';
const message = '{"user_id":123,"action":"purchase","amount":99.99}';
const signature = createHmac(message, secret);
// 'a3f4b2c1...'

// Verify:
verifyHmac(message, secret, signature)  // true
verifyHmac(message, secret, 'wrong')    // false

Python HMAC

import hmac
import hashlib

def create_hmac(message: str | bytes, secret: str | bytes) -> str:
    if isinstance(message, str): message = message.encode()
    if isinstance(secret, str): secret = secret.encode()
    
    return hmac.new(secret, message, hashlib.sha256).hexdigest()

def verify_hmac(message: str | bytes, secret: str | bytes, expected: str) -> bool:
    computed = create_hmac(message, secret)
    # hmac.compare_digest prevents timing attacks:
    return hmac.compare_digest(computed, expected)

# Example:
secret = 'my-super-secret-key'
message = '{"user_id":123}'
sig = create_hmac(message, secret)
verify_hmac(message, secret, sig)  # True

Webhook signature verification

GitHub, Stripe, and many services use HMAC to sign webhooks:

// GitHub webhook verification:
app.post('/webhook/github', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.GITHUB_WEBHOOK_SECRET;
  const signature = req.headers['x-hub-signature-256'];
  
  if (!signature) {
    return res.status(401).send('No signature');
  }
  
  const computed = `sha256=${createHmac(req.body, secret)}`;
  
  if (!crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature))) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process webhook:
  const payload = JSON.parse(req.body);
  console.log('Verified webhook:', payload.action);
  res.sendStatus(200);
});
// Stripe webhook verification:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.post('/webhook/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  
  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object;
    console.log('Payment succeeded:', paymentIntent.amount);
  }
  
  res.json({ received: true });
});

Signing API requests

// Example: sign API requests with timestamp to prevent replay attacks:
function signRequest(method, path, body, secret) {
  const timestamp = Math.floor(Date.now() / 1000);
  const message = `${timestamp}\n${method}\n${path}\n${body}`;
  const signature = createHmac(message, secret);
  
  return {
    'X-Timestamp': timestamp,
    'X-Signature': signature,
  };
}

// Verify on server (reject if timestamp is more than 5 minutes old):
function verifyRequest(req, secret) {
  const timestamp = parseInt(req.headers['x-timestamp']);
  const signature = req.headers['x-signature'];
  
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return false;  // Replay attack
  }
  
  const message = `${timestamp}\n${req.method}\n${req.path}\n${req.body}`;
  return verifyHmac(message, secret, signature);
}

HMAC vs JWT

HMACJWT
PurposeMessage authenticationAuthentication + claims
Contains dataNo (just a signature)Yes (payload is visible)
StatelessDepends on useYes
Common useWebhooks, API signingAuth tokens

Related posts

Related tool

Hash Generator

Generate MD5, SHA-1, SHA-256, and SHA-512 hashes client-side.

Written by Mian Ali Khalid. Part of the Dev Productivity pillar.