Cloudflare Docs
Workers
Visit Workers on GitHub
Set theme to dark (⇧+D)

Sign requests

Sign and verify a request using the HMAC and SHA-256 algorithms or return a 403.
// You will need some super-secret data to use as a symmetric key.
const encoder = new TextEncoder();
const secretKeyData = encoder.encode('my secret symmetric key');

// Convert a ByteString (a string whose code units are all in the range
// [0, 255]), to a Uint8Array. If you pass in a string with code units larger
// than 255, their values will overflow.
function byteStringToUint8Array(byteString) {
  const ui = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; ++i) {
    ui[i] = byteString.charCodeAt(i);
  }
  return ui;
}

async function verifyAndFetch(request) {
  const url = new URL(request.url);

  // If the path does not begin with our protected prefix, pass the request through
  if (!url.pathname.startsWith('/verify/')) {
    return fetch(request);
  }

  // Make sure you have the minimum necessary query parameters.
  if (!url.searchParams.has('mac') || !url.searchParams.has('expiry')) {
    return new Response('Missing query parameter', { status: 403 });
  }

  const key = await crypto.subtle.importKey(
    'raw',
    secretKeyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );

  // Extract the query parameters we need and run the HMAC algorithm on the
  // parts of the request we are authenticating: the path and the expiration
  // timestamp. It is crucial to pad the input data, for example, by adding a symbol
  // in-between the two fields that can never occur on the right side. In this
  // case, use the @ symbol to separate the fields.
  const expiry = Number(url.searchParams.get('expiry'));
  const dataToAuthenticate = `${url.pathname}@${expiry}`;

  // The received MAC is Base64-encoded, so you have to go to some trouble to
  // get it into a buffer type that crypto.subtle.verify() can read.
  const receivedMacBase64 = url.searchParams.get('mac');
  const receivedMac = byteStringToUint8Array(atob(receivedMacBase64));

  // Use crypto.subtle.verify() to guard against timing attacks. Since HMACs use
  // symmetric keys, you could implement this by calling crypto.subtle.sign() and
  // then doing a string comparison -- this is insecure, as string comparisons
  // bail out on the first mismatch, which leaks information to potential
  // attackers.
  const verified = await crypto.subtle.verify(
    'HMAC',
    key,
    receivedMac,
    encoder.encode(dataToAuthenticate)
  );

  if (!verified) {
    const body = 'Invalid MAC';
    return new Response(body, { status: 403 });
  }

  if (Date.now() > expiry) {
    const body = `URL expired at ${new Date(expiry)}`;
    return new Response(body, { status: 403 });
  }

  // you have verified the MAC and expiration time; you can now pass the request
  // through.
  return fetch(request);
}

addEventListener('fetch', event => {
  event.respondWith(verifyAndFetch(event.request));
});

Generating signed requests

You can generate signed requests from within a Worker using the Web Crypto APIs.

For request URLs beginning with /generate/, replace /generate/ with /verify/, sign the resulting path with its timestamp, and return the full, signed URL in the response body.

async function generateSignedUrl(url) {
  // You will need some super-secret data to use as a symmetric key.
  const encoder = new TextEncoder();
  const secretKeyData = encoder.encode('my secret symmetric key');
  const key = await crypto.subtle.importKey(
    'raw',
    secretKeyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  // Signed requests expire after one minute. Note that you could choose
  // expiration durations dynamically, depending on, for example, the path or a query
  // parameter.
  const expirationMs = 60000;
  const expiry = Date.now() + expirationMs;
  // The signature will be computed for the pathname and the expiry timestamp.
  // The two fields must be separated or padded to ensure that an attacker
  // will not be able to use the same signature for other pathname/expiry pairs.
  // The @ symbol is guaranteed not to appear in expiry, which is a (decimal)
  // number, so you can safely use it as a separator here. When combining more
  // fields, consider JSON.stringify-ing an array of the fields instead of
  // concatenating the values.
  const dataToAuthenticate = `${url.pathname}@${expiry}`;

  const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(dataToAuthenticate));

  // `mac` is an ArrayBuffer, so you need to make a few changes to get
  // it into a ByteString, and then a Base64-encoded string.
  const base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)));

  url.searchParams.set('mac', base64Mac);
  url.searchParams.set('expiry', expiry);

  return new Response(url);
}

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const prefix = '/generate/';
  if (url.pathname.startsWith(prefix)) {
    // Replace the "/generate/" path prefix with "/verify/", which we
    // use in the first example to recognize authenticated paths.
    url.pathname = `/verify/${url.pathname.slice(prefix.length)}`;
    event.respondWith(generateSignedUrl(url));
  } else {
    event.respondWith(fetch(event.request));
  }
});