Created
April 11, 2022 16:13
-
-
Save mike-pete/5495b49af536a9ea911427affe7a6eba to your computer and use it in GitHub Desktop.
An example of verifying JWTs from a Cloudflare Function.
This file contains hidden or 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
// the origonal gist that this code is based off of: | |
// https://gist.github.com/bcnzer/e6a7265fd368fa22ef960b17b9a76488 | |
// these are refrences for firebase stuff: | |
// https://www.googleapis.com/service_accounts/v1/jwk/[email protected] | |
// https://www.googleapis.com/robot/v1/metadata/x509/[email protected] | |
export default async function verifyJWT(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", expiryDate, currentDate) | |
return false | |
} | |
const { aud, iss, sub, auth_time: authTime } = token.payload | |
const validAud = "<your firebase aud>" | |
if (aud !== validAud) { | |
console.log("invalid aud", aud) | |
return false | |
} | |
if (iss !== `https://securetoken.google.com/${validAud}`) { | |
console.log("invalid iss", iss) | |
return false | |
} | |
if (typeof sub !== "string" || !sub) { | |
console.log("invalid sub", sub) | |
return false | |
} | |
let authTimeDate = new Date(authTime * 1000) | |
if (authTimeDate >= currentDate) { | |
console.log("invalid auth time", authTime) | |
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") { | |
console.log("No 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, "+")) | |
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 { kid } = token.header | |
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))) | |
// These JWKs are hard-coded for now. This is bad practice. | |
// In the future I plan to save them in Cloudflare's KV store. | |
// Then I'll add some logic to update the stored JWKs when keys stop getting validated | |
// (assuming the JWKs that were stored are now outdated). | |
const googleKeys = [ | |
{ | |
use: "sig", | |
kid: "6a4f87ff5d93fa6ea03e5c6e88eea0acd2a232a9", | |
alg: "RS256", | |
e: "AQAB", | |
n: "01-vTS0PZnEnDIKKERkRnSrj_bb33pRHgCzSHBWscGu7fA1GUGUi33imAjX2ugYUNJREeos4uswwSi-NEXta7xqg_dgEC5NPDeAXk1QlENQ0ZqaQK8_GmmQjrovkSm7uGxD1Ob9keSooxW6PxckB_0He2Ywh9avs2yStnmjNs_B6Ao_OcvGB1OTZTS2nY9lhjhTC1ijP3AJAEBf4hnPHSAN3kvvMglU0JT71tHJg2dg56muMyWkm3hjhersKSm_FCJgxa6_xrILB9Ok1U48YelUC2xUn2S9Vu17_yUnX0Mxrl_zM1XZ3tVUOphODPRXa3CD60PmOApj17kaMzexf8Q", | |
kty: "RSA", | |
}, | |
{ | |
alg: "RS256", | |
kid: "aafa812b16979180fc78209ea7ccab91e6843659", | |
e: "AQAB", | |
use: "sig", | |
n: "2Q7RA8IXDHq5XYyAHXm7mcbHZZ9iiBzkqLxHddM6uwkdSAE99zHJHlXYOcEnpCR9Tq4OCVeat857smtlYQcN0GyZxD8-k1TdSW5jmXyvqMOGFEj6myLnzOjRFQR6SVaOiojd9m72EGvg7N5sJyKItKLqa5KBjV5Cfnv05kh9ssMpgtL-_VwtywSOZKQ4LKcwh3EfvzsVH2aJSqtdtWASDPUxu76HrXK44BwSMLp8xXY1HYu3jlc2YVpeblGK9t-ayk81fWKD6pM5tHSEivxMf03dyqWM_3WjxvElsXqL5JEN58dwE47SFE6rIDi1mKhBiaCEguNESAVtcTAbLvHqQw", | |
kty: "RSA", | |
}, | |
{ | |
e: "AQAB", | |
kty: "RSA", | |
n: "tIiy5lHOYNynK0ISlQi0te6SWFUFFGEH73n1WnapdOiZx2c4Bm9S2f2iL2WEdjkKYqkEjHXlIRosF5mhg1jqU4aFquvsmhdP2yqdoYyYf2u5vvswzI3Ij0Guc8A1K9XamDN6egn3Pl7md9f6zhujbjP7oKHvDsm7s6QoxVKZpQmEDDiBcyCc6IUmMksBhO0h41lD4zFHZDxmEp3_B3g0EJp3Q80OuUBDzi7iMUktJ22m0qkyb0XJDRljrVsStfiilUCe7TaAs0jGdvX5Wbp4jAw1rE5jDHBEJrpAFEaNaPQOoRiu-OBNZukUhSackIjzdbeqmwtm_7IPoAp3kz3rYQ", | |
kid: "464117ac396bc71c8c59fb519f013e2b5bb6c6e1", | |
use: "sig", | |
alg: "RS256", | |
}, | |
] | |
const googleKey = googleKeys.find(({ kid: googleKid }) => kid === googleKid) | |
const jwk = { | |
alg: "RS256", | |
kty: "RSA", | |
key_ops: ["verify"], | |
use: "sig", | |
n: googleKey.n, | |
e: "AQAB", | |
kid: kid, | |
} | |
const key = await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, [ | |
"verify", | |
]) | |
const valid = crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data) | |
console.log("valid?", valid) | |
return { valid, userId: token.payload.user_id } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment