Skip to content

Instantly share code, notes, and snippets.

@mike-pete
Created April 11, 2022 16:13
Show Gist options
  • Save mike-pete/5495b49af536a9ea911427affe7a6eba to your computer and use it in GitHub Desktop.
Save mike-pete/5495b49af536a9ea911427affe7a6eba to your computer and use it in GitHub Desktop.
An example of verifying JWTs from a Cloudflare Function.
// 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/securetoken@system.gserviceaccount.com
// https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com
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