Skip to content

Instantly share code, notes, and snippets.

@karthik2804
Last active June 7, 2021 14:12
Show Gist options
  • Select an option

  • Save karthik2804/f4b663e1ca6dcbd35fb6fb0dd29e14e3 to your computer and use it in GitHub Desktop.

Select an option

Save karthik2804/f4b663e1ca6dcbd35fb6fb0dd29e14e3 to your computer and use it in GitHub Desktop.
Validate Firebase ID Tokens in CloudFlare Worker (JWT verification)
/*
* AUTHOR: Karthik Ganeshram <[email protected]>
* Date: 07-06-2021
* Description: A cloudflare worker template to validate Firebase ID tokens
* Usage: Request parameters: {method: POST, body:{"token: FIREBASE_ID_TOKEN}, headers: {"Content-Type": "application/json"}}
*/
/*
* API url for Firebase-auth public key in JWK format
*/
const googleJwkUrl = "https://www.googleapis.com/service_accounts/v1/jwk/[email protected]"
const firebaseProjectId = "<Firebase project ID>"
/*
* Algorithm Used by Firebase Auth
*/
const algo = {
name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' }
}
/*
* Helper functions for handling JWT
*/
/*
* Converts the Base64 String to array buffer
*/
let _base64ToArrayBuffer = (base64) => {
var binary_string = atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
/*
* Parses the Base64 string and provides a JSON
*/
let parseJwtSegment = (part) => {
let jsonSegment = decodeURIComponent(atob(part).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''))
return JSON.parse(jsonSegment)
}
/*
* JWT uses base64url, which is different from base 64 by 2 character where
* Base64 uses '+' amd '/' while Base64Url uses '-' and '_'
*/
let base64url2string = (string) => {
return string.replace(/-/g, '+').replace(/_/g, '/');
}
/*
* End of helper functions
*/
/*
* Verifies the signature of the token
* Returns true is the signature is verified else returns false
*/
let verifyJWT = async (data, signature, keyData) => {
let key = await crypto.subtle.importKey('jwk', keyData, algo, false, ['verify'])
let res = await crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, key, signature, new TextEncoder().encode(data))
return res
}
/*
* Validates the token header and payload claims
* refer https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library
* Does Not check if the token is properly base64url encoded
*/
function validateJWT(token, JWKs) {
let parts = token.split('.')
if (parts.length != 3) {
return
}
let JWTHeader = parseJwtSegment(base64url2string(parts[0]))
// Validate Header data
if (JWTHeader.alg != "RS256" || !JWKs[JWTHeader.kid] || JWTHeader.typ != "JWT") {
return
}
let JWTPayload = parseJwtSegment(base64url2string(parts[1]))
let currentDate = Date.now()
// Validate Payload data
if (JWTPayload.exp * 1000 <= currentDate || JWTPayload.iat * 1000 > currentDate || JWTPayload.aud != firebaseProjectId ||
JWTPayload.iss != "https://securetoken.google.com/" + firebaseProjectId ||
JWTPayload.sub == "" || JWTPayload.auth_time * 1000 > currentDate) {
return
}
// if token is valid, create data and signature for verification
let data = parts[0] + '.' + parts[1]
let signature = _base64ToArrayBuffer(base64url2string(parts[2]))
let keyData = JWKs[JWTHeader.kid]
let ret = { headers: JWTHeader, JWTPayload: JWTPayload, validationData: data, signature: signature, keyData: keyData }
return ret
}
async function handleRequest(event) {
let request = event.request
// Only process if post request
if (request.method != "POST") {
return new Response('Not Post method!', {
headers: { 'content-type': 'text/plain' },
})
}
let token = (await request.json()).token
// Cache the JWK object using the max-age header in the response
const cacheUrl = new URL(googleJwkUrl)
const cacheKey = new Request(cacheUrl.toString())
const cache = caches.default
// Check if cache of Firebase JWK exists and is valid
let response = await cache.match(cacheKey)
// If cache doesnt exist fetch it and store to cach
if (!response) {
response = await fetch(googleJwkUrl)
event.waitUntil(cache.put(cacheKey, response.clone()))
}
response = await response.json()
let JWKs = {}
// JWK is an object that holds the different JWK objects for different kid values
response.keys.map(k => JWKs[k.kid] = k)
// Verify if the token is a valid JWT
let JWT = validateJWT(token, JWKs)
let result
if (JWT) {
// Verify the Signature
result = await verifyJWT(JWT.validationData, JWT.signature, JWT.keyData)
}
// Do whatever necessary after validation of token
return new Response('The token is ' + (result ? "valid" : "invalid"), {
headers: { 'content-type': 'text/plain' },
})
}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event))
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment