Last update:2024-09-03 17:00:38
This example demonstrates how to secure access to specific website resources using Edge Cloud Apps Function and the Web Crypto API. It utilizes a pet shop’s “About Us” page located at http://ecatest.cdnetworks.com/ECA-test/pet-shop-website-template/about.html
as a practical example. Accessing this page requires a valid signature generated using the HMAC and SHA-256 algorithms. The Edge Cloud Apps Function can either sign outgoing requests on behalf of the client or verify incoming signed requests, acting as a security gatekeeper. Unauthorized requests will be met with an error page.
const SECRET_KEY = "my secret symmetric key";
// Handles requests, either signing or verifying based on the request method
async function handleRequest(request) {
if (request.method === "POST") {
return signRequest(request);
} else {
return verifyRequest(request);
}
}
// Verifies the request signature
async function verifyRequest(request) {
const url = new URL(request.url);
// Ensure the request contains the necessary query parameters
if (!url.searchParams.has("mac") || !url.searchParams.has("expiry")) {
return failPage("Missing query parameters");
}
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(SECRET_KEY);
// Import the key for signature verification
const key = await crypto.subtle.importKey(
"raw",
secretKeyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
// Extract data requiring verification: path and expiry timestamp
const expiry = Number(url.searchParams.get("expiry"));
const dataToAuthenticate = url.pathname + expiry;
// Convert the received Base64-encoded MAC to a Uint8Array
const receivedMacBase64 = url.searchParams.get("mac");
const receivedMac = byteStringToUint8Array(atob(receivedMacBase64));
// Utilize crypto.subtle.verify() for signature verification to prevent timing attacks
const verified = await crypto.subtle.verify(
"HMAC",
key,
receivedMac,
encoder.encode(dataToAuthenticate),
);
// If verification fails, return an error page
if (!verified) {
return failPage("Invalid MAC");
}
// Check if the URL has expired
if (Date.now() > expiry) {
return failPage(`URL expired at ${new Date(expiry)}`);
}
// Verification successful, allow access to the resource
return fetch(request);
}
// Convert a ByteString to a Uint8Array
function byteStringToUint8Array(byteString) {
const ui = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; ++i) {
ui[i] = byteString.charCodeAt(i);
}
return ui;
}
// Signs the request
async function signRequest(request) {
const url = new URL(request.url);
return generateSignedUrl(url);
}
// Generates a signed URL
async function generateSignedUrl(url) {
// Import the key for signature generation
const encoder = new TextEncoder();
const secretKeyData = encoder.encode(SECRET_KEY);
const key = await crypto.subtle.importKey(
"raw",
secretKeyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
// Set URL expiration time
const expirationMs = 18000; // 18 seconds
const expiry = Date.now() + expirationMs;
const dataToAuthenticate = url.pathname + expiry;
// Generate signature
const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(dataToAuthenticate));
// Convert the signature to a Base64-encoded string
const base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)));
// Add the signature and expiration time to the URL query parameters
url.searchParams.set("mac", base64Mac);
url.searchParams.set("expiry", expiry);
return new Response(url);
}
// Generate error page
function failPage(msg) {
let page = FAILPAGE.replace('{{error-msg}}', msg);
return new Response(page, {headers: {"Content-Type": "text/html; charset=utf-8"}});
}
// Error page HTML template
const FAILPAGE = `
<!DOCTYPE html>
<html>
<head>
<title>Error Page</title>
<style>
// ... Page styles ...
</style>
</head>
<body>
<div class="container">
<h1>Error</h1>
<p>The URL you are trying to access is not correctly signed: {{error-msg}}</p>
<div class="url-section">
<div class="url-wrapper">
<p><span class="url-display" id="current-url" style="display: block;"></span></p>
</div>
<div class="button-wrapper">
<button class="url-button" type="button" onclick="getSignedUrl()">Sign URL</button>
</div>
<div class="url-wrapper" id="signed-url-section">
<p><strong>Signed URL will expire in 18 seconds</strong></p>
<p><span class="url-display" id="signed-url"></span></p>
</div>
</div>
</div>
<script>
// ... Page script ...
</script>
</body>
</html>
`;
addEventListener("fetch", event => {
return event.respondWith(handleRequest(event.request));
});
Edge Cloud Apps Function fully support the Web Crypto API, enabling you to meet your encryption and decryption needs at the edge. You can use Edge Cloud Apps Functions to sign requests on behalf of the client or to validate incoming signed requests, effectively preventing unauthorized access (hotlinking protection).