diff --git a/kvs-conditional-read/ABUriMappingFunction.js b/kvs-conditional-read/ABUriMappingFunction.js index c2ad3bf..8dd348c 100644 --- a/kvs-conditional-read/ABUriMappingFunction.js +++ b/kvs-conditional-read/ABUriMappingFunction.js @@ -1,6 +1,6 @@ import cf from 'cloudfront'; -// Replace KVS_ID with actual KVS ID +// (Optional) Replace KVS_ID with actual KVS ID const kvsId = "KVS_ID"; // enable stickiness by setting a cookie from origin or using another edge function const stickinessCookieName = "appversion"; @@ -77,4 +77,4 @@ function log(message) { if (loggingEnabled) { console.log(message); } -} \ No newline at end of file +} diff --git a/kvs-conditional-read/README.md b/kvs-conditional-read/README.md new file mode 100644 index 0000000..823459c --- /dev/null +++ b/kvs-conditional-read/README.md @@ -0,0 +1,5 @@ +## Rewrite request URI + +**CloudFront Functions event type: viewer request** + +This example provides a dynamic request URI rewriting mechanism, allowing for A/B testing or gradual rollout of application versions, while also maintaining user stickiness to ensure a consistent experience. It rewrites the request URI based on a configuration stored in CloudFront KeyValueStore. To use this example, you [create a KeyValueStore](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-create.html) for your secret and [associate the KeyValueStore with the CloudFront function](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html). diff --git a/kvs-jwt-verify/README.md b/kvs-jwt-verify/README.md new file mode 100644 index 0000000..58f7d84 --- /dev/null +++ b/kvs-jwt-verify/README.md @@ -0,0 +1,60 @@ +## Verify a JSON Web Token (JWT) using SHA256 HMAC signature + +**CloudFront Functions event type: viewer request** + +This function validates a JSON Web Token (JWT) in the query string of the incoming request. It is compatible with the CloudFront Functions [JavaScript 2.0 runtime](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-20.html) and uses [CloudFront KeyValueStore](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions.html) to store the secret. Using your CloudFront KeyValueStore ID is optional. + +JWT is an open, industry standard [RFC 7519](https://tools.ietf.org/html/rfc7519) method for representing claims securely between two parties. You can use JWTs to validate that a viewer has the right access to view the content being requested. You can use this type of tokenization to give a user of your site a URL that is timebound. After the predetermined expiry time is reached, the user no longer has access to the content on that URL. + +This function has two components. First, your origin or application must be able to generate a JWT and append that token as a query string to the URL. Second, you must use this sample function (or some variation of this function) on a viewer request event type to validate the JWT in the query string, ensuring that the URL hasn't been changed or tampered with and the expiry time hasn't passed. If the token is valid and the expiry time hasn't passed, the request passes through to CloudFront and the request is served. If the token is invalid or the expiry time has passed, the function generates and serves a 401 Unauthorized response to the viewer. + +In this example, your origin or application establishes a JWT. We have provided a simple bash script for building a JWT called `generate-jwt.sh`. There are many libraries across multiple different languages for signing and verifying JWTs available on [jwt.io](https://jwt.io/). + +The output of `generate-jwt.sh` is the JWT that the function will validate. Append the output to the URL as a query string in the following format `token=` (for example, `token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJuYmYiOjE1MTYyMzkwMjIsImV4cCI6MTcxNjIzOTAyMn0.jyu6HjS95wU8iSofQ8nBlmPjFYODxn4PQAdFM-Cv8JY`). + +CloudFront already provides a [signed URLs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html) feature that you can use instead of this function. A signed URL can include additional information, such as an expiration date and time, start date and time, and client IP address. This gives you more control over access to your content. However, creating a signed URL creates long and complex URLs and is more computationally costly to produce. If you need a simple and lightweight way to validate timebound URLs, this function can be easier than using CloudFront signed URLs. + +**Testing the function** + +To validate that the function is working as expected, you can use the JSON test objects in the `test-objects` directory. To test, you can use the `test-function` CLI command as shown in the following example: + +``` +$ aws cloudfront test-function --if-match EXXXXXXXXXXXX --name kvs-jwt-verify --event-object fileb://kvs-jwt-verify/test-objects/valid-jwt.json +``` + +If the function has been set up correctly, you should see a log entry saying the token is valid in the `FunctionExecutionLogs` and JWT token removed in the `FunctionOutput` JSON object: +``` +{ + "TestResult": { + "FunctionSummary": { + "Name": "kvs-jwt-verify", + "Status": "UNASSOCIATED", + "FunctionConfig": { + "Comment": "", + "Runtime": "cloudfront-js-2.0", + "KeyValueStoreAssociations": { + "Quantity": 1, + "Quantity": 1, + "Items": [ + { + "KeyValueStoreARN": "arn:aws:cloudfront::123456789012:key-value-store/6ed3b692-38e9-4952-b89b-bea9cexample" + } + ] + } + }, + "FunctionMetadata": { + "FunctionARN": "arn:aws:cloudfront::123456789012:function/kvs-jwt-verify", + "Stage": "DEVELOPMENT", + "CreatedTime": "2021-04-09T22:02:12.937000+00:00", + "LastModifiedTime": "2021-04-09T22:09:19.277000+00:00" + } + }, + "ComputeUtilization": "19", + "FunctionExecutionLogs": [ + "Valid JWT token" + ], + "FunctionErrorMessage": "", + "FunctionOutput": "{\"request\":{\"headers\":{\"host\":{\"value\":\"www.example.com\"},\"accept\":{\"value\":\"text/html\"}},\"method\":\"GET\",\"querystring\":{\"test\":{\"value\":\"anotherQueryString\"}},\"uri\":\"/index.html\",\"cookies\":{}}}" + } +} +``` diff --git a/verify-jwt/test-objects/expired-jwt.json b/kvs-jwt-verify/test-objects/expired-jwt.json similarity index 99% rename from verify-jwt/test-objects/expired-jwt.json rename to kvs-jwt-verify/test-objects/expired-jwt.json index 9210544..fa98773 100644 --- a/verify-jwt/test-objects/expired-jwt.json +++ b/kvs-jwt-verify/test-objects/expired-jwt.json @@ -18,4 +18,4 @@ "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } } } -} \ No newline at end of file +} diff --git a/verify-jwt/test-objects/invalid-jwt.json b/kvs-jwt-verify/test-objects/invalid-jwt.json similarity index 99% rename from verify-jwt/test-objects/invalid-jwt.json rename to kvs-jwt-verify/test-objects/invalid-jwt.json index d116c87..a3e73d6 100644 --- a/verify-jwt/test-objects/invalid-jwt.json +++ b/kvs-jwt-verify/test-objects/invalid-jwt.json @@ -18,4 +18,4 @@ "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } } } -} \ No newline at end of file +} diff --git a/verify-jwt/test-objects/invalid-nbe-jwt.json b/kvs-jwt-verify/test-objects/invalid-nbe-jwt.json similarity index 99% rename from verify-jwt/test-objects/invalid-nbe-jwt.json rename to kvs-jwt-verify/test-objects/invalid-nbe-jwt.json index 8832ca8..b758257 100644 --- a/verify-jwt/test-objects/invalid-nbe-jwt.json +++ b/kvs-jwt-verify/test-objects/invalid-nbe-jwt.json @@ -18,4 +18,4 @@ "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } } } -} \ No newline at end of file +} diff --git a/verify-jwt/test-objects/missing-jwt.json b/kvs-jwt-verify/test-objects/missing-jwt.json similarity index 99% rename from verify-jwt/test-objects/missing-jwt.json rename to kvs-jwt-verify/test-objects/missing-jwt.json index bfed220..939c679 100644 --- a/verify-jwt/test-objects/missing-jwt.json +++ b/kvs-jwt-verify/test-objects/missing-jwt.json @@ -17,4 +17,4 @@ "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } } } -} \ No newline at end of file +} diff --git a/verify-jwt/test-objects/valid-jwt.json b/kvs-jwt-verify/test-objects/valid-jwt.json similarity index 99% rename from verify-jwt/test-objects/valid-jwt.json rename to kvs-jwt-verify/test-objects/valid-jwt.json index 022cc34..f5f7cd4 100644 --- a/verify-jwt/test-objects/valid-jwt.json +++ b/kvs-jwt-verify/test-objects/valid-jwt.json @@ -18,4 +18,4 @@ "accept": { "value": "text/html", "multivalue": [ { "value": "text/html" }, { "value": "application/xhtml+xml" } ] } } } -} \ No newline at end of file +} diff --git a/kvs-jwt-verify/verify-jwt.js b/kvs-jwt-verify/verify-jwt.js index 7b8bbde..5775c0b 100644 --- a/kvs-jwt-verify/verify-jwt.js +++ b/kvs-jwt-verify/verify-jwt.js @@ -1,17 +1,14 @@ import crypto from 'crypto'; import cf from 'cloudfront'; -// updated the original example from below URL to use KVS -// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-validate-token.html - //Response when JWT is not valid. const response401 = { statusCode: 401, statusDescription: 'Unauthorized' }; -// Replace the KVS_ID with your KVS ID -const kvsId = "KVS_ID"; +// Remember to associate the KVS with your function before calling the const kvsKey = 'jwt.secret'. +// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html const kvsKey = 'jwt.secret'; // set to true to enable console logging const loggingEnabled = false; @@ -67,12 +64,12 @@ function _constantTimeEquals(a, b) { if (a.length != b.length) { return false; } - + let xor = 0; for (let i = 0; i < a.length; i++) { xor |= (a.charCodeAt(i) ^ b.charCodeAt(i)); } - + return 0 === xor; } @@ -130,7 +127,7 @@ async function handler(event) { async function getSecret() { // initialize cloudfront kv store and get the key value try { - const kvsHandle = cf.kvs(kvsId); + const kvsHandle = cf.kvs(); return await kvsHandle.get(kvsKey); } catch (err) { log(`Error reading value for key: ${kvsKey}, error: ${err}`); @@ -143,4 +140,4 @@ function log(message) { if (loggingEnabled) { console.log(message); } -} \ No newline at end of file +} diff --git a/kvs-key-value-pairs/README.md b/kvs-key-value-pairs/README.md new file mode 100644 index 0000000..39688fd --- /dev/null +++ b/kvs-key-value-pairs/README.md @@ -0,0 +1,9 @@ +## Use key-value pairs + +**CloudFront Functions event type: viewer request** + +The example uses key-value pairs from an Amazon CloudFront [KeyValueStore](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions.html) in a CloudFront function. + +The example shows a function that uses the content of the URL in the HTTP request to look up a custom path in the key value store. CloudFront then uses that custom path to make the request. This function helps manage the multiple paths that are part of a website, such as updating the version of a blog platform on a website. For example, if the earlier blog has origin path ```/blog-v1``` and the new blog has origin path ```/blog-v2```, this function can look up the URL path of the incoming request and rewrite the URL path ```(/blog-v1)``` to the new version of the blog ```(/blog-v2)```. + +The example works with [JavaScript runtime 2.0](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-20.html). diff --git a/kvs-key-value-pairs/kvs-key-value-pairs.js b/kvs-key-value-pairs/kvs-key-value-pairs.js new file mode 100644 index 0000000..07c1978 --- /dev/null +++ b/kvs-key-value-pairs/kvs-key-value-pairs.js @@ -0,0 +1,26 @@ +import cf from 'cloudfront'; + +// This fails if there is no key value store associated with the function +const kvsHandle = cf.kvs(); + +// Remember to associate the KVS with your function before referencing KVS in your code. +// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html +async function handler(event) { + const request = event.request; + // Use the first segment of the pathname as key + // For example http(s)://domain//something/else + const pathSegments = request.uri.split('/') + const key = pathSegments[1] + try { + // Replace the first path of the pathname with the value of the key + // For example http(s)://domain//something/else + pathSegments[1] = await kvsHandle.get(key); + const newUri = pathSegments.join('/'); + console.log(`${request.uri} -> ${newUri}`) + request.uri = newUri; + } catch (err) { + // No change to the pathname if the key is not found + console.log(`${request.uri} | ${err}`); + } + return request; +} diff --git a/normalize-query-string-parameters/README.md b/normalize-query-string-parameters/README.md new file mode 100644 index 0000000..c1270f1 --- /dev/null +++ b/normalize-query-string-parameters/README.md @@ -0,0 +1,7 @@ +## Normalize query string parameters + +**CloudFront functions event type: viewer request** + +You can normalize query string parameters to improve the cache hit ratio. + +The following example works with JavaScript runtime 1.0 and 2.0. The example shows how to improve your cache hit ratio by putting the query strings in alphabetical order before CloudFront forwards requests to your origin. diff --git a/normalize-query-string-parameters/normalize-query-string.js b/normalize-query-string-parameters/normalize-query-string.js new file mode 100644 index 0000000..f5fb365 --- /dev/null +++ b/normalize-query-string-parameters/normalize-query-string.js @@ -0,0 +1,15 @@ +function handler(event) { + var qs=[]; + for (var key in event.request.querystring) { + if (event.request.querystring[key].multiValue) { + event.request.querystring[key].multiValue.forEach((mv) => {qs.push(key + "=" + mv.value)}); + } else { + qs.push(key + "=" + event.request.querystring[key].value); + } + }; + + event.request.querystring = qs.sort().join('&'); + + + return event.request; +} diff --git a/verify-jwt/README.md b/verify-jwt/README.md deleted file mode 100644 index 0afe066..0000000 --- a/verify-jwt/README.md +++ /dev/null @@ -1,51 +0,0 @@ -## Verify a JSON Web Token (JWT) using SHA256 HMAC signature - -**CloudFront Functions event type: viewer request** - -**Note:** We recommend using the [kvs-jwt-verify example](https://github.com/aws-samples/amazon-cloudfront-functions/tree/main/kvs-jwt-verify) instead. It uses Amazon CloudFront KeyValueStore to store the secret key, and is compatible with JavaScript 2.0 runtime. - -This function validates a JSON Web Token (JWT) in the query string of the incoming request. JWT is an open, industry standard [RFC 7519](https://tools.ietf.org/html/rfc7519) method for representing claims securely between two parties. You can use JWTs to validate that a viewer has the right access to view the content being requested. You can use this type of tokenization to give a user of your site a URL that is time bound. Once the predetermined expiry time is reached, the user no longer has access to the content on that URL. - -This function has two components. First, your origin or application must be able to generate a JWT and append that token as a query string to the URL. Second, you must use this sample function (or some variation of this function) on a viewer request event type to validate the JWT in the query string, ensuring that the URL hasn't been changed or tampered with and the expiry time hasn't passed. If the token is valid and the expiry time hasn't passed, the request passes through to CloudFront and the request is served. If the token is invalid or the expiry time has passed, the function generates and serves a 401 Unauthorized response to the viewer. - -In this example, your origin or application establish a JWT. We have provided a simple bash script for building a JWT called `generate-jwt.sh`. There are many libraries across multiple different languages for signing and verifying JWTs available on [jwt.io](https://jwt.io/). - -The output of `generate-jwt.sh` is the JWT that the function will validate. Append the output to the URL as a query string in the following format `token=` (e.g. `token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJuYmYiOjE1MTYyMzkwMjIsImV4cCI6MTcxNjIzOTAyMn0.jyu6HjS95wU8iSofQ8nBlmPjFYODxn4PQAdFM-Cv8JY`). - -CloudFront already provides a [signed URLs](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html) feature that you can use instead of this function. A signed URL can include additional information, such as an expiration date and time, start date and time, and client IP address, which gives you more control over access to your content. However, creating a signed URL creates long and complex URLs and is more computationally costly to produce. If you need a simple and lightweight way to validate time bound URLs, this function can be easier than using CloudFront signed URLs. - -**Testing the function** - -To validate that the function is working as expected, you can use the JSON test objects in the `test-objects` directory. To test, you can use the `test-function` CLI command as shows in the following example: - -``` -$ aws cloudfront test-function --if-match EXXXXXXXXXXXX --name verify-jwt --event-object fileb://verify-jwt/test-objects/valid-jwt.json -``` - -If the function has been set up correctly, you should see a log entry saying the token is valid in the `FunctionExecutionLogs` and JWT token removed in the `FunctionOutput` JSON object: -``` -{ - "TestResult": { - "FunctionSummary": { - "Name": "verify-jwt", - "Status": "UNPUBLISHED", - "FunctionConfig": { - "Comment": "", - "Runtime": "cloudfront-js-1.0" - }, - "FunctionMetadata": { - "FunctionARN": "arn:aws:cloudfront::1234567890:function/verify-jwt", - "Stage": "DEVELOPMENT", - "CreatedTime": "2021-04-09T22:02:12.937000+00:00", - "LastModifiedTime": "2021-04-09T22:09:19.277000+00:00" - } - }, - "ComputeUtilization": "19", - "FunctionExecutionLogs": [ - "Valid JWT token" - ], - "FunctionErrorMessage": "", - "FunctionOutput": "{\"request\":{\"headers\":{\"host\":{\"value\":\"www.example.com\"},\"accept\":{\"value\":\"text/html\"}},\"method\":\"GET\",\"querystring\":{\"test\":{\"value\":\"anotherQueryString\"}},\"uri\":\"/index.html\",\"cookies\":{}}}" - } -} -``` diff --git a/verify-jwt/generate-jwt.sh b/verify-jwt/generate-jwt.sh deleted file mode 100644 index 14500a7..0000000 --- a/verify-jwt/generate-jwt.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -# -# JWT Encoder Bash Script -# - -secret='LzdWGpAToQ1DqYuzHxE6YOqi7G3X2yvNBot9mCXfx5k' - -# Static header fields. -header='{ - "alg": "HS256", - "typ": "JWT" -}' - -payload='{ - "sub": "1234567890", - "name": "John Doe", - "iat": 1516239022, - "nbf" : 161623932, - "exp" : 1616239042 -}' - -base64_encode() -{ - declare input=${1:-$( payload.exp*1000) { - throw new Error('Token expired'); - } - } - - return payload; -} - -//Function to ensure a constant time comparison to prevent -//timing side channels. -function _constantTimeEquals(a, b) { - if (a.length != b.length) { - return false; - } - - var xor = 0; - for (var i = 0; i < a.length; i++) { - xor |= (a.charCodeAt(i) ^ b.charCodeAt(i)); - } - - return 0 === xor; -} - -function _verify(input, key, method, type, signature) { - if(type === "hmac") { - return _constantTimeEquals(signature, _sign(input, key, method)); - } - else { - throw new Error('Algorithm type not recognized'); - } -} - -function _sign(input, key, method) { - return crypto.createHmac(method, key).update(input).digest('base64url'); -} - -function _base64urlDecode(str) { - return String.bytesFrom(str, 'base64url') -} - -function handler(event) { - var request = event.request; - - //Secret key used to verify JWT token. - //Update with your own key. - var key = "LzdWGpAToQ1DqYuzHxE6YOqi7G3X2yvNBot9mCXfx5k"; - - // If no JWT token, then generate HTTP redirect 401 response. - if(!request.querystring.jwt) { - console.log("Error: No JWT in the querystring"); - return response401; - } - - var jwtToken = request.querystring.jwt.value; - - try{ - jwt_decode(jwtToken, key); - } - catch(e) { - console.log(e); - return response401; - } - - //Remove the JWT from the query string if valid and return. - delete request.querystring.jwt; - console.log("Valid JWT token"); - return request; -}