Edgio

Basic Request Signing

You may only deploy this edge function using CDN-as-Code due to Node.js dependencies. It will not work when deployed through the Edgio Console.
Request signing is a technique used to verify the authenticity and integrity of a request. It involves adding a signature to the request, which is generated using a secret key and specific request parameters. This signature can then be verified by the recipient to ensure that the request has not been tampered with and originated from a trusted source.
Request signing can be used in various scenarios, such as API authentication, secure communication between services, and preventing replay attacks. By including a signature with each request, both the sender and receiver can have confidence in the integrity and authenticity of the data being exchanged.

Router Configuration

In the Edgio router, you can use the edge_function feature to specify the path to the edge function that will handle the request signing and verification. Because this edge function is designed to handle both signing and verification, we’ll match any request beginning with /sign/ or /verify/, and capture the remaining path for use in the edge function.
JavaScriptroutes.js
1import {Router, edgioRoutes} from '@edgio/core';
2
3export default new Router()
4 .use(edgioRoutes)
5
6 .match(/\/(sign|verify)\/(.*)/, {
7 edge_function: './edge-functions/main.js',
8 });

Edge Function

The edge function will be responsible for generating a signed URL for the given request, or verifying the signature of a request and forwarding it to the origin. The edge function will be invoked for any request that matches the route above, so we’ll need to check the request path to determine whether we are signing or verifying the request.
In either case, we’ll need to generate a signature using a cryptographic hash function. In this example, we’ll use the HMAC-SHA1 algorithm, which is a widely used cryptographic hash function. The signature will be generated using a secret key, which should be defined as an environment variable in the Edgio Console. The secret key should never be shared publicly, and should be kept private to ensure that the signature cannot be forged.
The Edge Function runtime does not currently support a native crypto library, so a third-party library to generate the signature is needed. In this example, we’ll use the crypto-js library.
JavaScriptedge-functions/main.js
1import HmacSHA1 from 'crypto-js/hmac-sha1';
2import Base64 from 'crypto-js/enc-base64';
3
4export async function handleHttpRequest(request, context) {
5 // ** IMPORTANT **
6 // Secret key should be defined as an environment variable in the Edgio console
7 const secretKey = '$0m3th!ngS3cr3t'; // context.environmentVars.REQ_SIGNING_SECRET_KEY;
8
9 if (request.url.includes('/sign/')) {
10 return generateSignedUrl(request, secretKey);
11 }
12
13 return verifyAndFetch(request, secretKey);
14}
15
16/**
17 * Generates a signed URL for the given URL and secret key
18 * @param {URL} url
19 * @param {string} key
20 */
21async function generateSignedUrl(request, key) {
22 const url = new URL(request.url);
23
24 // Replace /sign/ with /verify/ in the URL since we are generating a signed URL for verification
25 url.pathname = url.pathname.replace('/sign/', '/verify/');
26
27 const expirationMs = 1000 * 60 * 5; // 5 minutes
28 const expiry = Date.now() + expirationMs;
29 const dataToAuthenticate = url.pathname + expiry;
30
31 const hash = HmacSHA1(dataToAuthenticate, key);
32 const base64Mac = Base64.stringify(hash);
33
34 url.searchParams.set('mac', base64Mac);
35 url.searchParams.set('expiry', expiry.toString());
36
37 // respond with the signed URL that can be used to verify the request
38 return new Response(url.toString());
39}
40
41/**
42 * Verifies the MAC and expiry of the given URL. If the URL is valid, the request is forwarded to the origin.
43 */
44async function verifyAndFetch(request, key) {
45 const invalidResponse = (reason) =>
46 new Response(`Invalid request - ${reason}`, {status: 403});
47 const url = new URL(request.url);
48
49 if (!url.searchParams.has('mac') || !url.searchParams.has('expiry')) {
50 return invalidResponse('Missing MAC or expiry');
51 }
52
53 const expiry = Number(url.searchParams.get('expiry'));
54 const dataToAuthenticate = url.pathname + expiry;
55
56 const receivedMacBase64 = url.searchParams.get('mac');
57 const receivedMac = Base64.parse(receivedMacBase64);
58
59 const hash = HmacSHA1(dataToAuthenticate, key);
60 const hashInBase64 = Base64.stringify(hash);
61
62 // Ensure that the MAC is valid
63 if (hashInBase64 !== receivedMacBase64) {
64 return invalidResponse('Invalid MAC');
65 }
66
67 // Ensure that the URL has not expired
68 if (Date.now() > expiry) {
69 return invalidResponse('URL has expired');
70 }
71
72 // Forward the remaining request path after **/verify/* to the origin
73 url.pathname = url.pathname.split('/verify/')[1];
74
75 return fetch(url.toString(), {
76 edgio: {origin: 'web'},
77 });
78}