const crypto = require('crypto') const fs = require('fs') const path = require('path') const jwt = require('jsonwebtoken') const axios = require('axios') const JWKS_URLS = [ 'https://www.googleapis.com/oauth2/v3/certs', 'https://www.googleapis.com/oauth2/v1/certs', ] const CACHE_FILE = path.join(__dirname, 'googleJwksCache.json') const CACHE_TTL_MS = 6 * 60 * 60 * 1000 let memoryCache = { keys: [], fetchedAt: 0 } function jwkToPem(jwk) { return crypto.createPublicKey({ key: jwk, format: 'jwk' }).export({ type: 'spki', format: 'pem', }) } function readDiskCache() { try { if (!fs.existsSync(CACHE_FILE)) return null const parsed = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')) if (!Array.isArray(parsed?.keys) || parsed.keys.length === 0) return null return parsed } catch (err) { console.warn('[googleTokenVerify] cache read failed:', err.message) return null } } function writeDiskCache(keys) { try { const payload = { fetchedAt: Date.now(), keys } fs.writeFileSync(CACHE_FILE, JSON.stringify(payload, null, 2)) memoryCache = payload } catch (err) { console.warn('[googleTokenVerify] cache write failed:', err.message) } } async function fetchJwksFromGoogle() { for (const url of JWKS_URLS) { try { const res = await axios.get(url, { timeout: 12000 }) if (Array.isArray(res.data?.keys) && res.data.keys.length > 0) { return res.data.keys } } catch (err) { console.warn('[googleTokenVerify] JWKS fetch failed:', url, err.message) } } return null } async function getGoogleJwksKeys(forceRefresh = false) { if ( !forceRefresh && memoryCache.keys.length > 0 && Date.now() - memoryCache.fetchedAt < CACHE_TTL_MS ) { return memoryCache.keys } const liveKeys = await fetchJwksFromGoogle() if (liveKeys?.length) { writeDiskCache(liveKeys) return liveKeys } const disk = readDiskCache() if (disk?.keys?.length) { memoryCache = disk return disk.keys } throw new Error( 'Google JWKS unavailable (server cannot reach googleapis.com). Cached keys missing.' ) } function verifyWithJwk(idToken, jwk, audiences) { const pem = jwkToPem(jwk) return jwt.verify(idToken, pem, { algorithms: ['RS256'], audience: audiences, issuer: ['https://accounts.google.com', 'accounts.google.com'], }) } async function verifyGoogleIdToken(idToken, audiences) { if (!idToken || typeof idToken !== 'string') { throw new Error('Invalid token') } const allowedAudiences = ( Array.isArray(audiences) ? audiences : [audiences] ).filter(Boolean) if (allowedAudiences.length === 0) { throw new Error('Google client ID is not configured') } const decoded = jwt.decode(idToken, { complete: true }) const kid = decoded?.header?.kid if (!kid) { throw new Error('Invalid token header') } let keys = await getGoogleJwksKeys(false) let jwk = keys.find((key) => key.kid === kid) if (!jwk) { keys = await getGoogleJwksKeys(true) jwk = keys.find((key) => key.kid === kid) } if (!jwk) { throw new Error('Signing key not found for Google token') } return verifyWithJwk(idToken, jwk, allowedAudiences) } module.exports = { verifyGoogleIdToken, getGoogleJwksKeys, }