Sign requests
Verify a signed request using the HMAC and SHA-256 algorithms or return a 403.
export default {async fetch(request) {// 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;}const url = new URL(request.url);// If the path does not begin with our protected prefix, pass the request throughif (!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);},};
const handler: ExportedHandler = {async fetch(request) {// 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;}const url = new URL(request.url);// If the path does not begin with our protected prefix, pass the request throughif (!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);},};export default handler;
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.
export default {async fetch(request) {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.let base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)));// must convert "+" to "-" as urls encode "+" as " "base64Mac = base64Mac.replaceAll("+", "-");url.searchParams.set("mac", base64Mac);url.searchParams.set("expiry", expiry);return new Response(url);}const url = new URL(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)}`;return await generateSignedUrl(url);} else {return fetch(request);}},};
const handler: ExportedHandler = {async fetch(request: Request) {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.let base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)));// must convert "+" to "-" as urls encode "+" as " "base64Mac = base64Mac.replaceAll("+", "-");url.searchParams.set("mac", base64Mac);url.searchParams.set("expiry", expiry);return new Response(url);}const url = new URL(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)}`;return await generateSignedUrl(url);} else {return fetch(request);}},};export default handler;