Skip to content

Commit

Permalink
add CF recipes
Browse files Browse the repository at this point in the history
  • Loading branch information
tootedom committed Jan 15, 2019
1 parent 5843fb8 commit 8046fff
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 0 deletions.
18 changes: 18 additions & 0 deletions lib/__tests__/cloudworker-e2e.test.js
Expand Up @@ -363,4 +363,22 @@ describe('cloudworker-e2e', async () => {
await server.close()
cb()
})

test('test cloudflare generate and verify recipes', async (cb) => {
const cf_verify_script = utils.read(path.join(__dirname, 'fixtures/cf-verify.js'))
const cf_generate_script = utils.read(path.join(__dirname, 'fixtures/cf-generate.js'))
const server = new Cloudworker(cf_generate_script).listen(8080)
const res = await axios.get('http://localhost:8080/generate/bob', defaultAxiosOpts)
expect(res.status).toEqual(200)
const verifyUrl = res.data
await server.close()

const server2 = new Cloudworker(cf_verify_script).listen(8080)
const verifiedRes = await axios.get(verifyUrl, defaultAxiosOpts)
expect(verifiedRes.status).toEqual(200)
expect(verifiedRes.data).toEqual(true)

await server2.close()
cb()
})
})
44 changes: 44 additions & 0 deletions lib/__tests__/fixtures/cf-generate.js
@@ -0,0 +1,44 @@
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))
}
})

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 hoops to get
// it into a ByteString, 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)
}
74 changes: 74 additions & 0 deletions lib/__tests__/fixtures/cf-verify.js
@@ -0,0 +1,74 @@
addEventListener('fetch', event => {
event.respondWith(verifyAndFetch(event.request))
})

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

// If the path doesn't 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 })
}

// 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, [ "verify" ]
)

// Extract the query parameters we need and run the HMAC algorithm on the
// parts of the request we're 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've verified the MAC and expiration time; we're good to pass the request
// through.
return new Response(true,{status: 200})
}

// 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
}

0 comments on commit 8046fff

Please sign in to comment.