@connectedcars/jwtutils
Advanced tools
Comparing version 1.0.8 to 1.0.9
@@ -16,3 +16,3 @@ #!/usr/bin/env node | ||
let issuer = process.argv[5] | ||
let audiences = process.argv[6] | ||
let audiences = process.argv[6].split(',') | ||
@@ -19,0 +19,0 @@ let publicKey = fs.readFileSync(publicKeyPath) |
@@ -42,2 +42,4 @@ 'use strict' | ||
// Read stderr | ||
let stderrStr = '' | ||
let errorData = [] | ||
@@ -48,10 +50,9 @@ jwtEncode.stderr.on('data', data => { | ||
jwtEncode.stderr.on('data', () => { | ||
let error = Buffer.concat(errorData).toString('utf8') | ||
if (error != '') { | ||
done(new Error(error)) | ||
} | ||
stderrStr = Buffer.concat(errorData).toString('utf8') | ||
}) | ||
// Read token | ||
let stdoutStr = '' | ||
let decodedData = [] | ||
let error | ||
jwtEncode.stdout.on('data', data => { | ||
@@ -61,8 +62,20 @@ decodedData.push(data) | ||
jwtEncode.stdout.on('end', () => { | ||
let decodedBodyStr = Buffer.concat(decodedData).toString('utf8').trim() | ||
let decodedBody = JSON.parse(decodedBodyStr) | ||
expect(jwtBody, 'to equal', decodedBody) | ||
done() | ||
try { | ||
stdoutStr = Buffer.concat(decodedData).toString('utf8').trim() | ||
let decodedBody = JSON.parse(stdoutStr) | ||
expect(decodedBody, 'to equal', jwtBody) | ||
} catch (e) { | ||
error = e | ||
} | ||
}) | ||
jwtEncode.on('exit', (code, signal) => { | ||
if (error) { | ||
console.log(`stdout:${stdoutStr}\nstderr:${stderrStr}\nexit:${code}`) | ||
done(error) | ||
} else { | ||
done() | ||
} | ||
}) | ||
}).slow(2000) | ||
}) |
{ | ||
"name": "@connectedcars/jwtutils", | ||
"version": "1.0.8", | ||
"version": "1.0.9", | ||
"description": "Zero dependency JWT encoding/decoding for Node", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -121,2 +121,10 @@ # node-jwtutils | ||
'1@RS256': publicKey // Fx. use key from before | ||
}, | ||
'https://jwt.io/custom': { // Overwrite default validation for this issuer | ||
'1@RS256': { // Same options can also be used directly with decode | ||
publicKey: publicKey, | ||
expiresMax: 3600, // Don't allow token that has a lifetime over 1 hour | ||
expiresSkew: 600, // Allow tokens that expired up to 10 minutes ago | ||
nbfIatSkew: 300, // Allow tokens that has nbf or iat in the future by up to 5 minutes | ||
} | ||
} | ||
@@ -123,0 +131,0 @@ } |
@@ -81,2 +81,19 @@ 'use strict' | ||
'4@RS256': rsaOtherPublicKey.substr(2) | ||
}, | ||
'test@custom.com': { | ||
'1@RS256': { | ||
publicKey: rsaPublicKey, | ||
expiresSkew: 600, | ||
expiresMax: 86400 | ||
} | ||
}, | ||
'test@expired.com': { | ||
'1@RS256': { | ||
publicKey: rsaPublicKey, | ||
validators: { | ||
exp: () => { | ||
throw new JwtVerifyError('Always expired') | ||
} | ||
} | ||
} | ||
} | ||
@@ -89,5 +106,8 @@ } | ||
let jwt = oldJwtUtils.encode(rsaPrivateKey, jwtHeader, jwtBody) | ||
let decodedJwtBody = oldJwtUtils.decode(jwt, pubKeys, [ | ||
'https://host/oauth/token' | ||
]) | ||
let decodedJwtBody = oldJwtUtils.decode( | ||
jwt, | ||
pubKeys, | ||
['https://host/oauth/token'], | ||
300 | ||
) | ||
expect(jwtBody, 'to equal', decodedJwtBody) | ||
@@ -138,2 +158,82 @@ }) | ||
}) | ||
it('success with expired token', () => { | ||
let customJwtBody = Object.assign({}, jwtBody) | ||
customJwtBody.iss = 'test@custom.com' | ||
customJwtBody.exp -= 600 | ||
let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, customJwtBody) | ||
let decodedJwtBody = JwtUtils.decode(jwt, pubKeys, [ | ||
'https://host/oauth/token' | ||
]) | ||
expect(customJwtBody, 'to equal', decodedJwtBody) | ||
}) | ||
it('token outside maximum expires', () => { | ||
let customJwtBody = Object.assign({}, jwtBody) | ||
customJwtBody.iss = 'test@custom.com' | ||
customJwtBody.exp += 172800 | ||
let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, customJwtBody) | ||
expect( | ||
() => { | ||
JwtUtils.decode(jwt, pubKeys, ['https://host/oauth/token']) | ||
}, | ||
'to throw', | ||
`Expires in the future by more than 86400 seconds` | ||
) | ||
}) | ||
it('always fail with expired', () => { | ||
let customJwtBody = Object.assign({}, jwtBody) | ||
customJwtBody.iss = 'test@expired.com' | ||
let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, customJwtBody) | ||
expect( | ||
() => { | ||
JwtUtils.decode(jwt, pubKeys, ['https://host/oauth/token']) | ||
}, | ||
'to throw', | ||
`Always expired` | ||
) | ||
}) | ||
it('token outside maximum expires using decode options', () => { | ||
let customJwtBody = Object.assign({}, jwtBody) | ||
customJwtBody.exp += 172800 | ||
let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, customJwtBody) | ||
expect( | ||
() => { | ||
JwtUtils.decode(jwt, pubKeys, ['https://host/oauth/token'], { | ||
expiresMax: 600 | ||
}) | ||
}, | ||
'to throw', | ||
`Expires in the future by more than 600 seconds` | ||
) | ||
}) | ||
it('token outside maximum expires using nbf', () => { | ||
let customJwtBody = Object.assign({}, jwtBody) | ||
customJwtBody.exp += 172800 | ||
customJwtBody.nbf = customJwtBody.iat | ||
delete customJwtBody.iat | ||
let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, customJwtBody) | ||
expect( | ||
() => { | ||
JwtUtils.decode(jwt, pubKeys, ['https://host/oauth/token'], { | ||
expiresMax: 600 | ||
}) | ||
}, | ||
'to throw', | ||
`Expires in the future by more than 600 seconds` | ||
) | ||
}) | ||
it('token outside maximum expires using unixNow', () => { | ||
let customJwtBody = Object.assign({}, jwtBody) | ||
customJwtBody.exp += 172800 | ||
delete customJwtBody.iat | ||
let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, customJwtBody) | ||
expect( | ||
() => { | ||
JwtUtils.decode(jwt, pubKeys, ['https://host/oauth/token'], { | ||
expiresMax: 600 | ||
}) | ||
}, | ||
'to throw', | ||
`Expires in the future by more than 600 seconds` | ||
) | ||
}) | ||
it('unknown aud', () => { | ||
@@ -235,3 +335,3 @@ let jwt = JwtUtils.encode(rsaPrivateKey, jwtHeader, jwtBody) | ||
let customJwtHeader = Object.assign({}, jwtHeader) | ||
customJwtHeader.kid = 3 | ||
customJwtHeader.kid = '3' | ||
let jwt = JwtUtils.encode(rsaPrivateKey, customJwtHeader, jwtBody) | ||
@@ -248,3 +348,3 @@ expect( | ||
let customJwtHeader = Object.assign({}, jwtHeader) | ||
customJwtHeader.kid = 2 | ||
customJwtHeader.kid = '2' | ||
let jwt = JwtUtils.encode(rsaPrivateKey, customJwtHeader, jwtBody) | ||
@@ -273,3 +373,3 @@ expect( | ||
let customJwtHeader = Object.assign({}, jwtHeader) | ||
customJwtHeader.kid = 4 | ||
customJwtHeader.kid = '4' | ||
let jwt = JwtUtils.encode(rsaPrivateKey, customJwtHeader, jwtBody) | ||
@@ -280,4 +380,4 @@ expect( | ||
}, | ||
'to throw', | ||
'PEM_read_bio_PUBKEY failed' | ||
'to throw a', | ||
Error | ||
) | ||
@@ -284,0 +384,0 @@ }) |
@@ -41,2 +41,8 @@ const express = require('express') | ||
'1@ES256': ecPublicKey | ||
}, | ||
'http://localhost/oauth/token/moreexpires': { | ||
'1@ES256': { | ||
publicKey: ecPublicKey, | ||
expiresSkew: 600 | ||
} | ||
} | ||
@@ -43,0 +49,0 @@ } |
@@ -8,3 +8,4 @@ 'use strict' | ||
function jwtDecode(jwt, publicKeys, audiences, nbfIatSkrew = 300) { | ||
const defaultOptions = { expiresSkew: 0, expiresMax: 0, nbfIatSkew: 300 } | ||
function jwtDecode(jwt, publicKeys, audiences, options = defaultOptions) { | ||
if (typeof jwt !== 'string') { | ||
@@ -20,2 +21,17 @@ throw new Error('jwt needs to a string') | ||
if (!Array.isArray(audiences)) { | ||
throw new Error('audiences needs to be an array of allowed audiences') | ||
} | ||
if (typeof options === 'number') { | ||
// Backwards compatibility with old api | ||
options = { | ||
nbfIatSkew: options | ||
} | ||
} | ||
if (typeof options !== 'object' || Array.isArray(publicKeys)) { | ||
throw new Error('options needs to a map of { nbfIatSkew: 300, ... }') | ||
} | ||
let parts = jwt.split(/\./) | ||
@@ -65,11 +81,14 @@ if (parts.length !== 3) { | ||
let signature = base64UrlSafe.decode(parts[2]) | ||
// Find public key | ||
let pubkey = | ||
typeof header.kid === 'string' | ||
? issuer[`${header.kid}@${header.alg}`] | ||
: issuer[`default@${header.alg}`] | ||
const verifier = crypto.createVerify(algo) | ||
verifier.write(`${parts[0]}.${parts[1]}`, 'utf8') | ||
verifier.end() | ||
let issuerOptions = {} | ||
if (typeof pubkey === 'object' && pubkey !== null && pubkey.publicKey) { | ||
issuerOptions = pubkey | ||
pubkey = pubkey.publicKey | ||
} | ||
let pubkey = header.kid | ||
? issuer[`${header.kid}@${header.alg}`] | ||
: issuer[`default@${header.alg}`] | ||
if (!pubkey) { | ||
@@ -81,2 +100,7 @@ throw new JwtVerifyError( | ||
// Validate signature | ||
let signature = base64UrlSafe.decode(parts[2]) | ||
const verifier = crypto.createVerify(algo) | ||
verifier.write(`${parts[0]}.${parts[1]}`, 'utf8') | ||
verifier.end() | ||
if (!verifier.verify(pubkey, signature)) { | ||
@@ -88,32 +112,62 @@ throw new JwtVerifyError( | ||
let auds = Array.isArray(body.aud) ? body.aud : [body.aud] | ||
if (!auds.some(aud => audiences.includes(aud))) { | ||
throw new JwtVerifyError(`Unknown audience '${auds.join(',')}'`) | ||
let unixNow = Math.floor(Date.now() / 1000) | ||
let validators = { | ||
aud: validateAudience, | ||
exp: validateExpires, | ||
iat: validateIssuedAt, | ||
nbf: validateNotBefore | ||
} | ||
Object.assign(validators, options.validators || {}) | ||
Object.assign(validators, issuerOptions.validators || {}) | ||
let unixNow = Math.floor(Date.now() / 1000) | ||
let validationOptions = {} | ||
Object.assign(validationOptions, options) | ||
Object.assign(validationOptions, issuerOptions) | ||
if (body.iat && body.iat > unixNow + nbfIatSkrew) { | ||
validators.aud(body, audiences, validationOptions) | ||
validators.iat(body, unixNow, validationOptions) | ||
validators.nbf(body, unixNow, validationOptions) | ||
validators.exp(body, unixNow, validationOptions) | ||
return body | ||
} | ||
function validateNotBefore(body, unixNow, options) { | ||
if (body.nbf && body.nbf > unixNow + options.nbfIatSkew) { | ||
throw new JwtVerifyError( | ||
`Issued at in the future by more than ${nbfIatSkrew} seconds` | ||
`Not before in the future by more than ${options.nbfIatSkew} seconds` | ||
) | ||
} | ||
} | ||
if (body.nbf && body.nbf > unixNow + nbfIatSkrew) { | ||
function validateIssuedAt(body, unixNow, options) { | ||
if (body.iat && body.iat > unixNow + options.nbfIatSkew) { | ||
throw new JwtVerifyError( | ||
`Not before in the future by more than ${nbfIatSkrew} seconds` | ||
`Issued at in the future by more than ${options.nbfIatSkew} seconds` | ||
) | ||
} | ||
} | ||
function validateAudience(body, audiences, options) { | ||
let auds = Array.isArray(body.aud) ? body.aud : [body.aud] | ||
if (!auds.some(aud => audiences.includes(aud))) { | ||
throw new JwtVerifyError(`Unknown audience '${auds.join(',')}'`) | ||
} | ||
} | ||
function validateExpires(body, unixNow, options = {}) { | ||
if (!body.exp) { | ||
throw new JwtVerifyError(`No expires set on token`) | ||
} | ||
if (body.exp <= unixNow) { | ||
let notBefore = body.iat || body.nbf || unixNow | ||
if (options.expiresMax && body.exp > notBefore + options.expiresMax) { | ||
throw new JwtVerifyError( | ||
`Expires in the future by more than ${options.expiresMax} seconds` | ||
) | ||
} | ||
if (body.exp + (options.expiresSkew || 0) <= unixNow) { | ||
throw new JwtVerifyError('Token has expired') | ||
} | ||
return body | ||
} | ||
module.exports = jwtDecode |
@@ -35,2 +35,20 @@ 'use strict' | ||
}) | ||
it('invalid audiences input', () => { | ||
expect( | ||
() => { | ||
JwtUtils.decode(testJwt, pubKeys, '') | ||
}, | ||
'to throw', | ||
'audiences needs to be an array of allowed audiences' | ||
) | ||
}) | ||
it('invalid options input', () => { | ||
expect( | ||
() => { | ||
JwtUtils.decode(testJwt, pubKeys, audiences, '') | ||
}, | ||
'to throw', | ||
'options needs to a map of { nbfIatSkew: 300, ... }' | ||
) | ||
}) | ||
it('too few spaces', () => { | ||
@@ -37,0 +55,0 @@ expect( |
62530
30
1420
217