Last active
June 7, 2021 14:12
-
-
Save karthik2804/f4b663e1ca6dcbd35fb6fb0dd29e14e3 to your computer and use it in GitHub Desktop.
Validate Firebase ID Tokens in CloudFlare Worker (JWT verification)
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
| /* | |
| * 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