Skip to content

Commit 792db6f

Browse files
authored
Merge pull request #43 from graviavu/main
KVS Samples for Conditional Read and JWT Verify
2 parents 68b3457 + bf8651e commit 792db6f

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import cf from 'cloudfront';
2+
3+
// Replace KVS_ID with actual KVS ID
4+
const kvsId = "KVS_ID";
5+
// enable stickiness by setting a cookie from origin or using another edge function
6+
const stickinessCookieName = "appversion";
7+
// set to true to enable console logging
8+
const loggingEnabled = false;
9+
10+
// function rewrites the request uri based on configuration in KVS
11+
// example config in KVS in key:value format
12+
// "latest": {"a_weightage": .8, "a_url": "v1", "b_url": "v2"}
13+
// given above key and value in KVS the request uri will be rewritten
14+
// for example http(s)://domain/latest/something/else will be rewritten as http(s)://domain/v1/something/else or http(s)://domain/v2/something/else depending on weightage
15+
// if no configuration is found, then the request is returned as is
16+
async function handler(event) {
17+
// NOTE: This example function is for a viewer request event trigger.
18+
// Choose viewer request for event trigger when you associate this function with a distribution.
19+
const request = event.request;
20+
const pathSegments = request.uri.split('/');
21+
const key = pathSegments[1];
22+
23+
// if empty path segment or if there is valid stickiness cookie
24+
// then skip call to KVS and let the request continue.
25+
if (!key || hasValidSticknessCookie(request.cookies[stickinessCookieName], key)) {
26+
return event.request;
27+
}
28+
29+
try {
30+
// get the prefix replacement from KVS
31+
const replacement = await getPathPrefixByWeightage(key);
32+
if (!replacement) {
33+
return event.request;
34+
}
35+
//Replace the first path with the replacement
36+
pathSegments[1] = replacement;
37+
log(`using prefix ${pathSegments[1]}`)
38+
const newUri = pathSegments.join('/');
39+
log(`${request.uri} -> ${newUri}`);
40+
request.uri = newUri;
41+
42+
return request;
43+
} catch (err) {
44+
// No change to the path if the key is not found or any other error
45+
log(`request uri: ${request.uri}, error: ${err}`);
46+
}
47+
// no change to path - return request
48+
return event.request;
49+
}
50+
51+
// function to get the prefix from KVS
52+
async function getPathPrefixByWeightage(key) {
53+
const kvsHandle = cf.kvs(kvsId);
54+
// get the weightage config from KVS
55+
const kvsResponse = await kvsHandle.get(key);
56+
const weightageConfig = JSON.parse(kvsResponse);
57+
// no configuration - return null
58+
if (!weightageConfig || !isFinite(weightageConfig.a_weightage)) {
59+
return null;
60+
}
61+
// return the url based on weightage
62+
// return null if no url is configured
63+
if (Math.random() <= weightageConfig.a_weightage) {
64+
return weightageConfig.a_url ? weightageConfig.a_url: null;
65+
} else {
66+
return weightageConfig.b_url ? weightageConfig.b_url : null;
67+
}
68+
}
69+
70+
// function to check if the stickiness cookie is valid
71+
function hasValidSticknessCookie(stickinessCookie, pathSegment) {
72+
// if the value exists and it matches pathSegment
73+
return (stickinessCookie && stickinessCookie.value === pathSegment)
74+
}
75+
76+
function log(message) {
77+
if (loggingEnabled) {
78+
console.log(message);
79+
}
80+
}

kvs-jwt-verify/generate-jwt.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env bash
2+
3+
#
4+
# JWT Encoder Bash Script
5+
#
6+
7+
secret='REPLACE_WITH_YOUR_KEY'
8+
9+
# initialize time as epoch milliseconds
10+
time=$(date +%s)
11+
exp=$(($time+3600))
12+
# Static header fields.
13+
header='{
14+
"alg": "HS256",
15+
"typ": "JWT"
16+
}'
17+
18+
19+
payload='{
20+
"sub": "1234567890",
21+
"name": "John Doe",
22+
"iat": 1516239022,
23+
"nbf" : 161623932,
24+
"exp" :EXP_TIME
25+
}'
26+
27+
# Replace EPOCH_TIME with the actual epoch time.
28+
payload=$(echo "${payload}" | sed "s/EXP_TIME/${exp}/g")
29+
30+
31+
base64_encode()
32+
{
33+
declare input=${1:-$(</dev/stdin)}
34+
# Use `tr` to URL encode the output from base64.
35+
printf '%s' "${input}" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n'
36+
}
37+
38+
json() {
39+
declare input=${1:-$(</dev/stdin)}
40+
printf '%s' "${input}"
41+
}
42+
43+
hmacsha256_sign()
44+
{
45+
declare input=${1:-$(</dev/stdin)}
46+
printf '%s' "${input}" | openssl dgst -binary -sha256 -hmac "${secret}"
47+
}
48+
49+
header_base64=$(echo "${header}" | json | base64_encode)
50+
payload_base64=$(echo "${payload}" | json | base64_encode)
51+
52+
header_payload=$(echo "${header_base64}.${payload_base64}")
53+
signature=$(echo "${header_payload}" | hmacsha256_sign | base64_encode)
54+
55+
echo "${header_payload}.${signature}"

kvs-jwt-verify/verify-jwt.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import crypto from 'crypto';
2+
import cf from 'cloudfront';
3+
4+
// updated the original example from below URL to use KVS
5+
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-validate-token.html
6+
7+
//Response when JWT is not valid.
8+
const response401 = {
9+
statusCode: 401,
10+
statusDescription: 'Unauthorized'
11+
};
12+
13+
// Replace the KVS_ID with your KVS ID
14+
const kvsId = "KVS_ID";
15+
const kvsKey = 'jwt.secret';
16+
// set to true to enable console logging
17+
const loggingEnabled = false;
18+
19+
20+
function jwt_decode(token, key, noVerify, algorithm) {
21+
// check token
22+
if (!token) {
23+
throw new Error('No token supplied');
24+
}
25+
// check segments
26+
const segments = token.split('.');
27+
if (segments.length !== 3) {
28+
throw new Error('Not enough or too many segments');
29+
}
30+
31+
// All segment should be base64
32+
const headerSeg = segments[0];
33+
const payloadSeg = segments[1];
34+
const signatureSeg = segments[2];
35+
36+
// base64 decode and parse JSON
37+
const payload = JSON.parse(_base64urlDecode(payloadSeg));
38+
39+
if (!noVerify) {
40+
const signingMethod = 'sha256';
41+
const signingType = 'hmac';
42+
43+
// Verify signature. `sign` will return base64 string.
44+
const signingInput = [headerSeg, payloadSeg].join('.');
45+
46+
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
47+
throw new Error('Signature verification failed');
48+
}
49+
50+
// Support for nbf and exp claims.
51+
// According to the RFC, they should be in seconds.
52+
if (payload.nbf && Date.now() < payload.nbf*1000) {
53+
throw new Error('Token not yet active');
54+
}
55+
56+
if (payload.exp && Date.now() > payload.exp*1000) {
57+
throw new Error('Token expired');
58+
}
59+
}
60+
61+
return payload;
62+
}
63+
64+
//Function to ensure a constant time comparison to prevent
65+
//timing side channels.
66+
function _constantTimeEquals(a, b) {
67+
if (a.length != b.length) {
68+
return false;
69+
}
70+
71+
let xor = 0;
72+
for (let i = 0; i < a.length; i++) {
73+
xor |= (a.charCodeAt(i) ^ b.charCodeAt(i));
74+
}
75+
76+
return 0 === xor;
77+
}
78+
79+
function _verify(input, key, method, type, signature) {
80+
if(type === "hmac") {
81+
return _constantTimeEquals(signature, _sign(input, key, method));
82+
}
83+
else {
84+
throw new Error('Algorithm type not recognized');
85+
}
86+
}
87+
88+
function _sign(input, key, method) {
89+
return crypto.createHmac(method, key).update(input).digest('base64url');
90+
}
91+
92+
function _base64urlDecode(str) {
93+
return Buffer.from(str, 'base64url')
94+
}
95+
96+
async function handler(event) {
97+
let request = event.request;
98+
99+
//Secret key used to verify JWT token.
100+
//Update with your own key.
101+
const secret_key = await getSecret()
102+
103+
if(!secret_key) {
104+
return response401;
105+
}
106+
107+
// If no JWT token, then generate HTTP redirect 401 response.
108+
if(!request.querystring.jwt) {
109+
log("Error: No JWT in the querystring");
110+
return response401;
111+
}
112+
113+
const jwtToken = request.querystring.jwt.value;
114+
115+
try{
116+
jwt_decode(jwtToken, secret_key);
117+
}
118+
catch(e) {
119+
log(e);
120+
return response401;
121+
}
122+
123+
//Remove the JWT from the query string if valid and return.
124+
delete request.querystring.jwt;
125+
log("Valid JWT token");
126+
return request;
127+
}
128+
129+
// get secret from key value store
130+
async function getSecret() {
131+
// initialize cloudfront kv store and get the key value
132+
try {
133+
const kvsHandle = cf.kvs(kvsId);
134+
return await kvsHandle.get(kvsKey);
135+
} catch (err) {
136+
log(`Error reading value for key: ${kvsKey}, error: ${err}`);
137+
return null;
138+
}
139+
140+
}
141+
142+
function log(message) {
143+
if (loggingEnabled) {
144+
console.log(message);
145+
}
146+
}

0 commit comments

Comments
 (0)