Skip to content

Instantly share code, notes, and snippets.

@putrikarunia
Last active November 3, 2020 01:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save putrikarunia/f48bfec650d80bfc79d27c1c61c6feae to your computer and use it in GitHub Desktop.
Save putrikarunia/f48bfec650d80bfc79d27c1c61c6feae to your computer and use it in GitHub Desktop.
// ======================================================
// Validating the JWT Token
// ======================================================
// USAGE
// const resp = await validateJWT(access_token, apiKeyID)
//
// resp = { valid: true }
// or
// resp = { valid: false, reason: "error message" }
const CotterBaseURL = 'https://www.cotter.app/api/v0'
const CotterJWTKID = 'SPACE_JWT_PUBLIC:8028AAA3-EC2D-4BAA-BE7A-7C8359CCB9F9'
const jwksPath = '/token/jwks'
const COTTER_DOMAIN = 'https://www.cotter.app'
let cacheKeys = undefined
const getPublicKeys = async (cotterBaseURL) => {
if (!cacheKeys) {
const url = `${cotterBaseURL}${jwksPath}`
const resp = await fetch(url)
const r = await resp.json()
const newKeys = r.keys.reduce((agg, current) => {
agg[current.kid] = current
return agg
},
{})
cacheKeys = newKeys
return newKeys
} else {
return cacheKeys
}
}
const decodeJWTPayload = function (token) {
var output = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
switch (output.length % 4) {
case 0:
break
case 2:
output += '=='
break
case 3:
output += '='
break
default:
throw 'Illegal base64url string!'
}
const result = atob(output)
try {
return JSON.parse(decodeURIComponent(escape(result)))
} catch (err) {
console.log(err)
return JSON.parse(result)
}
}
function decodeJWT(token) {
const parts = token.split('.')
const header = JSON.parse(atob(parts[0]))
const payload = decodeJWTPayload(token)
const signature = atob(parts[2].replace(/_/g, '/').replace(/-/g, '+'))
return {
header: header,
payload: payload,
signature: signature,
raw: { header: parts[0], payload: parts[1], signature: parts[2] },
}
}
const validateJWTPayload = (token, apiKeyID) => {
try {
const dateInSecs = (d) => Math.ceil(Number(d) / 1000)
const date = new Date()
let iss = token.iss
iss = iss.endsWith('/') ? iss.slice(0, -1) : iss
if (iss !== COTTER_DOMAIN) {
throw new Error(
`Token iss value (${iss}) doesn't match COTTER_DOMAIN (${COTTER_DOMAIN})`,
)
}
if (apiKeyID && apiKeyID.length > 0 && token.aud !== apiKeyID) {
throw new Error(`Token aud value (${token.aud}) doesn't match API_KEY_ID`)
}
if (token.exp < dateInSecs(date)) {
throw new Error(`Token exp value is before current time`)
}
return true
} catch (err) {
console.log(err.message)
return false
}
}
const validateJWTSignature = async (token) => {
const encoder = new TextEncoder()
const data = encoder.encode([token.raw.header, token.raw.payload].join('.'))
const signature = new Uint8Array(
Array.from(token.signature).map((c) => c.charCodeAt(0)),
)
const jwtKeys = await getPublicKeys(CotterBaseURL)
const jwk = jwtKeys[CotterJWTKID]
const key = await crypto.subtle.importKey(
'jwk',
jwk,
{ name: 'ECDSA', namedCurve: 'P-256' },
false,
['verify'],
)
return crypto.subtle.verify(
{ name: 'ECDSA', hash: 'SHA-256' },
key,
signature,
data,
)
}
const validateJWT = async (tokenStr, apiKey) => {
const tokenRaw = decodeJWT(tokenStr)
try {
const validSignature = await validateJWTSignature(tokenRaw)
if (!validSignature) throw new Error('Invalid JWT Signature')
const validToken = validateJWTPayload(tokenRaw.payload, apiKey)
if (!validToken) throw new Error('Invalid JWT token')
return { valid: true }
} catch (e) {
return { valid: false, reason: e.toString() }
}
}
async function checkJWT(request, API_KEY_ID) {
// 1) Check if the authorization header exists
const auth = request.headers.get("Authorization")
if (!auth) throw new Error("Authorization header missing")
const bearer = auth ? auth.split(" ") : [];
const token = bearer && bearer.length > 0 ? bearer[1] : null;
// 2) Check if the access token is valid
const resp = await validateJWT(token, API_KEY_ID);
return resp.valid
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment