Files
modstagram-back/utils/googleTokenVerify.js
payacom 3f416a7a60 full
2026-08-31 20:45:41 +03:30

133 lines
3.2 KiB
JavaScript

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,
}