← blog
JavaScript Web Crypto API Zero dipendenze Sicurezza

HMAC-SHA256 with Web Crypto API in JavaScript

Sign and verify a payload using HMAC-SHA256 with only the browser's native cryptographic API β€” crypto.subtle. No external libraries, no dependencies. Works in all modern browsers and Node.js 18+.

The problem

You need to authenticate a token or verify data integrity, but don't want to add dependencies like crypto-js or jsonwebtoken. The Web Crypto API is already in the browser β€” just use it.

Sign a payload

hmac.js β€” firma
/**
 * Firma un payload con HMAC-SHA256.
 * @param {string} secret  – chiave segreta condivisa
 * @param {string} payload – stringa da firmare (es. JSON.stringify di un oggetto)
 * @returns {Promise<string>} firma in hex
 */
async function hmacSign(secret, payload) {
  const enc = new TextEncoder();

  const key = await crypto.subtle.importKey(
    'raw',
    enc.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,          // non estraibile
    ['sign']
  );

  const sig = await crypto.subtle.sign('HMAC', key, enc.encode(payload));

  // Converte ArrayBuffer β†’ stringa hex
  return [...new Uint8Array(sig)]
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

Verify the signature

hmac.js β€” verifica
/**
 * Verifica che tokenHex corrisponda a HMAC-SHA256(secret, payload).
 * Usa una comparazione a tempo costante per prevenire timing attacks.
 * @returns {Promise<boolean>}
 */
async function hmacVerify(secret, payload, tokenHex) {
  const expected = await hmacSign(secret, payload);

  // Confronto a lunghezza fissa β€” evita timing attacks
  if (expected.length !== tokenHex.length) return false;
  let diff = 0;
  for (let i = 0; i < expected.length; i++) {
    diff |= expected.charCodeAt(i) ^ tokenHex.charCodeAt(i);
  }
  return diff === 0;
}

Full example: signed URL with expiry

example.js β€” usage
const SECRET = 'chiave-segreta-condivisa';

// ── MITTENTE: crea token ──────────────────────────────
async function buildAuthUrl(userId, targetUrl) {
  const payload = JSON.stringify({
    uid: userId,
    exp: Date.now() + 5 * 60 * 1000 // scade in 5 minuti
  });

  const b64    = btoa(payload);
  const sig    = await hmacSign(SECRET, b64);
  const token  = `${b64}.${sig}`;

  return `${targetUrl}?auth=${encodeURIComponent(token)}`;
}

// ── DESTINATARIO: legge e verifica token ──────────────
async function parseAuthToken(token) {
  const [b64, sig] = token.split('.');
  if (!b64 || !sig) throw new Error('Token malformato');

  const valid = await hmacVerify(SECRET, b64, sig);
  if (!valid) throw new Error('Firma non valida');

  const data = JSON.parse(atob(b64));
  if (Date.now() > data.exp) throw new Error('Token scaduto');

  return data; // { uid: '...', exp: ... }
}

When to use it: cross-app authentication, webhook validation, expiring signed URLs, data integrity verification between client and server.

Warning: the secret in client-side code is visible to anyone who inspects the bundle. For public apps, sign server-side using a backend (e.g. Firebase Cloud Functions or a Node.js endpoint).

Why crypto.subtle instead of btoa()?

btoa() is just Base64 encoding β€” it signs nothing and guarantees no integrity. crypto.subtle.sign() uses real cryptographic primitives: the key is imported in a non-extractable form, operations happen in a protected context of the JS engine.

Related article
HMAC token between two separate Firebase PWAs β€” full real-world case
β†’

When to use HMAC-SHA256 with the Web Crypto API

This snippet shows how to sign and verify a payload with HMAC-SHA256 using the browser's native Web Crypto API, with no external libraries, useful for verifying the authenticity of data exchanged between client and server.

When to use it

When NOT to use it

Alternatives

Common mistakes

Frequently asked questions about HMAC-SHA256 with the Web Crypto API

Just signs it: HMAC-SHA256 guarantees the payload hasn't been tampered with and comes from someone holding the secret key, but doesn't hide the content, which remains readable by anyone who intercepts it.

No, the native Web Crypto API (crypto.subtle) supports HMAC-SHA256 directly, with no need for third-party crypto libraries.

A normal string comparison stops checking at the first differing character, indirectly leaking information through response time: a timing-safe comparison avoids this type of attack.