Skip to content

Instantly share code, notes, and snippets.

@sempostma
Last active May 30, 2020 09:23
Show Gist options
  • Save sempostma/b5b6492ddb805d71daa5e60f32c7788c to your computer and use it in GitHub Desktop.
Save sempostma/b5b6492ddb805d71daa5e60f32c7788c to your computer and use it in GitHub Desktop.
Validate firebase requests using jwt.
/*
You can obtain a token, client-side like this:
const { currentUser } = firebase.auth()
if (!currentUser) throw new Error('User has to be logged in.')
const idToken = await currentUser.getIdToken()
*/
const http = require('http')
const https = require('https')
const crypto = require('crypto')
const publicKeysUrl = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
const PORT = process.env.PORT || '8000'
const HEADERS = {
'Content-Type': 'text/json',
'Access-Control-Allow-Methods': 'POST, GET',
'Access-Control-Allow-Credentials': 'false',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept'
}
const replaceAll = (input, searchValue, replaceValue) => {
return input
.toString()
.split(searchValue)
.join(replaceValue)
}
const isBadInput = value => {
typeof value === 'string'
&& value.trim() !== ''
}
const requestPublicTokens = () => new Promise((resolve, reject) => {
https.get(publicKeysUrl, res => {
const { headers: { expires } } = res
if (res.statusCode !== 200) return reject(new Error(res.statusMessage))
let result = ''
res.on('error', error => {
reject(error)
})
res.on('data', data => result += data)
res.on('end', () => {
const publicKeys = JSON.parse(result)
const expiresAt = Date.parse(expires)
resolve({ publicKeys, expiresAt })
})
})
})
const getPublicTokensMemoized = () => {
let value = { expiresAt: 0, publicKeys: null }
return async () => {
if (Date.now() > value.expiresAt) {
value = await requestPublicTokens()
}
return value.publicKeys
}
}
const fromBase64 = value => {
try {
return Buffer.from(
jwtToValidBase64(value),
'base64').toString('utf8')
} catch (err) {
throw new Error('JWT token has an invalid base64 encoding.')
}
}
const parse = value => {
try {
return JSON.parse(
fromBase64(
value
)
)
} catch (err) {
logError(err)
throw new Error('decoded JWT token contains invalid JSON syntax.')
}
}
const jwtToValidBase64 = base64Url => {
base64Url = base64Url.toString()
const padding = 4 - base64Url.length % 4
if (padding !== 4) {
base64Url = base64Url + '==='.slice(0, padding)
}
return replaceAll(replaceAll(base64Url, '-', '+'), '_', '/')
}
const getPublicKeys = getPublicTokensMemoized()
const authenticate = async (authorization) => {
if (isBadInput(authorization)) {
throw new Error('No jwt token.')
}
const [rawHeader, rawBody, rawSignature] = authorization
.replace('Bearer ', '')
.replace('JWT ', '')
.trim()
.split('.')
const { kid, alg, typ } = parse(rawHeader)
if (!alg.includes('RS')) throw new Error(`Unsupported algorithm ${alg}. This example only allows RSxxx`)
const bits = alg.replace('RS', '')
const publicKeys = await getPublicKeys()
const publicKey = typeof publicKeys === 'string' ? publicKeys : publicKeys[kid]
const verifier = crypto.createVerify('RSA-SHA' + bits)
const signature = jwtToValidBase64(rawSignature)
verifier.update(rawHeader + '.' + rawBody)
const isValid = verifier.verify(publicKey, signature, 'base64')
if (!isValid) throw new Error('Invalid jwt token, verification was unsuccessful.')
return parse(rawBody)
}
const server = http.createServer(async (req, res) => {
const { headers: { authorization }, url, method, } = req
const CORS = {
'Access-Control-Allow-Origin': req.headers.origin,
}
if (method === 'OPTIONS') {
res.writeHead(200, { ...HEADERS, ...CORS })
res.end()
return
}
let token;
try {
token = await authenticate(authorization)
} catch (err) {
const status = 401
const title = err.message
res.writeHead(status, title, { ...HEADERS, ...CORS })
res.write(JSON.stringify({ errors: [{ status, title }] }))
res.end()
return
}
res.writeHead(200, { ...HEADERS, ...CORS })
res.write(JSON.stringify({ data: token }))
res.end()
})
server.listen(PORT)
console.log(`Listening to port: ${PORT}`)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment