Skip to content
Workers
Visit Workers on GitHub
Set theme to dark (⇧+D)

Signing requests

Sign and verify a request using the HMAC and SHA-256 algorithms or return a 403.

// We 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, just pass the request  // through.  if (!url.pathname.startsWith("/verify/")) {    return fetch(request)  }
  // Make sure we 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.  const expiry = Number(url.searchParams.get("expiry"))  const dataToAuthenticate = url.pathname + expiry
  // The received MAC is Base64-encoded, so we 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, we 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 })  }
  // We have verified the MAC and expiration time; we are good to pass the request  // through.  return fetch(request)}
addEventListener("fetch", event => {  event.respondWith(verifyAndFetch(event.request))})

Generating signed requests

Typically, signed requests are delivered to the user in some out-of-band way, such as email or are generated by the user themselves if they possess the symmetric key. You can also generate signed requests from within a Workers app.

For request URLs beginning with /generate/, we 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) {  // We"ll 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, e.g. the path or a query  // parameter.  const expirationMs = 60000  const expiry = Date.now() + expirationMs  const dataToAuthenticate = url.pathname + expiry
  const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(dataToAuthenticate))
  // `mac` is an ArrayBuffer, so we need to jump through a couple of hoops 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))  }})