Last active
July 2, 2024 10:18
-
-
Save bcnzer/e6a7265fd368fa22ef960b17b9a76488 to your computer and use it in GitHub Desktop.
Sample Cloudflare worker that gets the JWT, ensures it hasn't expired, decrypts it and returns a result
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event.request)) | |
}) | |
// Following code is a modified version of that found at https://blog.cloudflare.com/dronedeploy-and-cloudflare-workers/ | |
/** | |
* Fetch and log a request | |
* @param {Request} request | |
*/ | |
async function handleRequest(request) { | |
let isValid = await isValidJwt(request) | |
if (!isValid) { | |
// It is immediately failing here, which is great. The worker doesn't bother hitting your API | |
console.log('is NOT valid') | |
return new Response('Invalid JWT', { status: 403 }) | |
} else { | |
console.log('is valid') | |
} | |
console.log('Got request', request) | |
const response = await fetch(request) | |
console.log('Got response', response) | |
return response | |
} | |
/** | |
* Parse the JWT and validate it. | |
* | |
* We are just checking that the signature is valid, but you can do more that. | |
* For example, check that the payload has the expected entries or if the signature is expired.. | |
*/ | |
async function isValidJwt(request) { | |
const encodedToken = getJwt(request); | |
if (encodedToken === null) { | |
return false | |
} | |
const token = decodeJwt(encodedToken); | |
// Is the token expired? | |
let expiryDate = new Date(token.payload.exp * 1000) | |
let currentDate = new Date(Date.now()) | |
if (expiryDate <= currentDate) { | |
console.log('expired token') | |
return false | |
} | |
return isValidJwtSignature(token) | |
} | |
/** | |
* For this example, the JWT is passed in as part of the Authorization header, | |
* after the Bearer scheme. | |
* Parse the JWT out of the header and return it. | |
*/ | |
function getJwt(request) { | |
const authHeader = request.headers.get('Authorization'); | |
if (!authHeader || authHeader.substring(0, 6) !== 'Bearer') { | |
return null | |
} | |
return authHeader.substring(6).trim() | |
} | |
/** | |
* Parse and decode a JWT. | |
* A JWT is three, base64 encoded, strings concatenated with ‘.’: | |
* a header, a payload, and the signature. | |
* The signature is “URL safe”, in that ‘/+’ characters have been replaced by ‘_-’ | |
* | |
* Steps: | |
* 1. Split the token at the ‘.’ character | |
* 2. Base64 decode the individual parts | |
* 3. Retain the raw Bas64 encoded strings to verify the signature | |
*/ | |
function decodeJwt(token) { | |
const parts = token.split('.'); | |
const header = JSON.parse(atob(parts[0])); | |
const payload = JSON.parse(atob(parts[1])); | |
const signature = atob(parts[2].replace(/_/g, '/').replace(/-/g, '+')); | |
console.log(header) | |
return { | |
header: header, | |
payload: payload, | |
signature: signature, | |
raw: { header: parts[0], payload: parts[1], signature: parts[2] } | |
} | |
} | |
/** | |
* Validate the JWT. | |
* | |
* Steps: | |
* Reconstruct the signed message from the Base64 encoded strings. | |
* Load the RSA public key into the crypto library. | |
* Verify the signature with the message and the key. | |
*/ | |
async function isValidJwtSignature(token) { | |
const encoder = new TextEncoder(); | |
const data = encoder.encode([token.raw.header, token.raw.payload].join('.')); | |
const signature = new Uint8Array(Array.from(token.signature).map(c => c.charCodeAt(0))); | |
/* | |
const jwk = { | |
alg: 'RS256', | |
e: 'AQAB', | |
ext: true, | |
key_ops: ['verify'], | |
kty: 'RSA', | |
n: RSA_PUBLIC_KEY | |
}; | |
*/ | |
// You need to JWK data with whatever is your public RSA key. If you're using Auth0 you | |
// can download it from https://[your_domain].auth0.com/.well-known/jwks.json | |
const jwk = { | |
alg: "RS256", | |
kty: "RSA", | |
key_ops: ['verify'], | |
use: "sig", | |
x5c: ["REPLACE-ME-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], | |
n: "REPLACE-ME-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", | |
e: "AQAB", | |
kid: "REPLACE-ME-ccccccccccccccccccccccccccccccccccccccccccccccccc", | |
x5t: "REPLACE-ME-ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" | |
} | |
const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']); | |
return crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data) | |
} |
Thanks a lot for this, it was a life saver.
I was using this for an Auth0 integration and came across an issue where auth0 getIdTokenClaims would return invalid base64 strings, which led to the atob functions throwing an error. I believe it happens with other providers too.
If anyone else has the same issue you can solve it by using a package like js-base64's "decode" to replacing the atob functions. Or implementing something similar yourself.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for sharing 🙏
Alternatively there's tsndr/cloudflare-worker-jwt with signing support