Comparing version 1.0.11 to 2.0.0
@@ -17,14 +17,14 @@ /** | ||
**/ | ||
'use strict'; | ||
'use strict' | ||
const AuthenticationError = function (message, error) { | ||
Error.call(this, message); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.name = 'AuthenticationError'; | ||
this.message = message; | ||
if (error) this.inner = error; | ||
}; | ||
Error.call(this, message) | ||
Error.captureStackTrace(this, this.constructor) | ||
this.name = 'AuthenticationError' | ||
this.message = message | ||
if (error) this.inner = error | ||
} | ||
AuthenticationError.prototype = Object.create(Error.prototype); | ||
AuthenticationError.prototype.constructor = AuthenticationError; | ||
AuthenticationError.prototype = Object.create(Error.prototype) | ||
AuthenticationError.prototype.constructor = AuthenticationError | ||
module.exports = AuthenticationError; | ||
module.exports = AuthenticationError |
@@ -18,8 +18,7 @@ /** | ||
'use strict' | ||
const debug = require('debug')('byu-jwt-cache') | ||
const debug = require('debug')('byu-jwt-cache') | ||
module.exports = Cache | ||
function Cache() { | ||
function Cache () { | ||
const cache = {} | ||
@@ -29,13 +28,13 @@ const data = { | ||
timeoutId: null, | ||
ttl: 10, // 10 minute default | ||
ttl: 10, // 10 minute default | ||
value: null | ||
} | ||
process.on('exit', () => shutdown('exit', data)) // app is closing | ||
process.on('SIGINT', () => shutdown('SIGINT', data)) // catches ctrl+c event | ||
process.on('SIGBREAK', () => shutdown('SIGBREAK', data)) // catches Windows ctrl+c event | ||
process.on('SIGUSR1', () => shutdown('SIGUSR1', data)) // catches "kill pid" | ||
process.on('SIGUSR2', () => shutdown('SIGUSR2', data)) // catches "kill pid" | ||
process.on('exit', () => shutdown('exit', data)) // app is closing | ||
process.on('SIGINT', () => shutdown('SIGINT', data)) // catches ctrl+c event | ||
process.on('SIGBREAK', () => shutdown('SIGBREAK', data)) // catches Windows ctrl+c event | ||
process.on('SIGUSR1', () => shutdown('SIGUSR1', data)) // catches "kill pid" | ||
process.on('SIGUSR2', () => shutdown('SIGUSR2', data)) // catches "kill pid" | ||
cache.clearCache = function() { | ||
cache.clearCache = function () { | ||
clearCache(data) | ||
@@ -45,4 +44,4 @@ clearTimeout(data.timeoutId) | ||
cache.getCache = function() { | ||
const value = data.value; | ||
cache.getCache = function () { | ||
const value = data.value | ||
debug('cache retrieved') | ||
@@ -52,3 +51,3 @@ return value | ||
cache.setCache = function(value) { | ||
cache.setCache = function (value) { | ||
if (data.ttl > 0) { | ||
@@ -61,7 +60,7 @@ data.value = value | ||
cache.getTTL = function() { | ||
cache.getTTL = function () { | ||
return data.ttl | ||
} | ||
cache.setTTL = function(ttl) { | ||
cache.setTTL = function (ttl) { | ||
data.ttl = ttl > 0 ? ttl : 0 | ||
@@ -74,3 +73,3 @@ if (Date.now() + ttlInMilliseconds(data) < data.endTime) refreshCache(data) | ||
function clearCache(data) { | ||
function clearCache (data) { | ||
debug('cache cleared') | ||
@@ -80,3 +79,3 @@ data.value = null | ||
function refreshCache(data) { | ||
function refreshCache (data) { | ||
debug('cache updated') | ||
@@ -89,8 +88,8 @@ const ttl = ttlInMilliseconds(data) | ||
function ttlInMilliseconds(data) { | ||
function ttlInMilliseconds (data) { | ||
return data.ttl * 60000 | ||
} | ||
function shutdown(mode, data) { | ||
function shutdown (mode, data) { | ||
clearTimeout(data.timeoutId) | ||
} | ||
} |
450
lib/index.js
@@ -18,9 +18,12 @@ /** | ||
'use strict' | ||
const AuthenticationError = require('./AuthenticationError') | ||
const Cache = require('./cache') | ||
const debug = require('debug')('byu-jwt') | ||
const jsonWebToken = require('jsonwebtoken') | ||
const pem = require('pem') | ||
const request = require('./request') | ||
const AuthenticationError = require('./AuthenticationError') | ||
const Cache = require('./cache') | ||
const debug = require('debug')('byu-jwt') | ||
const jsonWebToken = require('jsonwebtoken') | ||
const request = require('./request') | ||
const { promisify } = require('util') | ||
const pemGetPublicKey = promisify(require('pem').getPublicKey) | ||
const jsonWebTokenVerify = promisify(jsonWebToken.verify) | ||
const BYU_JWT_CURRENT = { name: '', key: 'current', header: 'x-jwt-assertion' } | ||
@@ -32,3 +35,3 @@ const BYU_JWT_ORIGINAL = { name: 'Original', key: 'original', header: 'x-jwt-assertion-original' } | ||
function ByuJWT(options) { | ||
function ByuJWT (options) { | ||
const byuJwt = {} | ||
@@ -38,5 +41,5 @@ | ||
if (!options) options = {} | ||
if (!options.hasOwnProperty('basePath')) options.basePath = '' | ||
if (!options.hasOwnProperty('cacheTTL')) options.cacheTTL = 10 | ||
if (!options.hasOwnProperty('development')) options.development = false | ||
if (!Object.hasOwnProperty.call(options, 'basePath')) options.basePath = '' | ||
if (!Object.hasOwnProperty.call(options, 'cacheTTL')) options.cacheTTL = 10 | ||
if (!Object.hasOwnProperty.call(options, 'development')) options.development = false | ||
@@ -53,56 +56,66 @@ // validate options | ||
// set cache TTL | ||
byuJwt.cache = Cache() | ||
byuJwt.cache.setTTL(options.cacheTTL) | ||
byuJwt.cache = { openId: Cache(), byuCert: Cache() } | ||
byuJwt.cache.openId.setTTL(options.cacheTTL) | ||
byuJwt.cache.byuCert.setTTL(options.cacheTTL) | ||
byuJwt.authenticate = headers => { | ||
return init(byuJwt.cache) | ||
.then(openIdConfig => authenticate(byuJwt.options, openIdConfig, headers)) | ||
byuJwt.authenticate = async (headers) => { | ||
return authenticate(byuJwt.options, byuJwt.cache, headers) | ||
} | ||
byuJwt.authenticateUAPIMiddleware = function(req, res, next) { | ||
byuJwt.authenticateUAPIMiddleware = async function (req, res, next) { | ||
debug('running authenticateUAPIMiddleware') | ||
byuJwt.authenticate(req.headers) | ||
.then(verifiedJWTs => { | ||
req.verifiedJWTs = verifiedJWTs | ||
debug('completed authenticateUAPIMiddleware') | ||
next() | ||
}) | ||
.catch(err => { | ||
console.error(err.stack) | ||
const response = err instanceof AuthenticationError | ||
? { code: 401, message: err.message } | ||
: { code: 500, message: 'Error determining authentication' } | ||
debug('failed authenticateUAPIMiddleware: ' + err.stack) | ||
res.status(response.code).send({ metadata: { validation_response: response } }) | ||
}) | ||
try { | ||
req.verifiedJWTs = await byuJwt.authenticate(req.headers) | ||
debug('completed authenticateUAPIMiddleware') | ||
next() | ||
} catch (err) { | ||
console.error(err.stack) | ||
const response = err instanceof AuthenticationError | ||
? { code: 401, message: err.message } | ||
: { code: 500, message: 'Error determining authentication' } | ||
debug('failed authenticateUAPIMiddleware: ' + err.stack) | ||
res.status(response.code).send({ metadata: { validation_response: response } }) | ||
} | ||
} | ||
byuJwt.decodeJWT = function(jwt) { | ||
return init(byuJwt.cache) | ||
.then(openIdConfig => decodeJWT(byuJwt.options, openIdConfig, jwt)) | ||
byuJwt.decodeJWT = async function (jwt) { | ||
return decodeJWT(byuJwt.options, byuJwt.cache, jwt) | ||
} | ||
byuJwt.getOpenIdConfiguration = function() { | ||
byuJwt.getOpenIdConfiguration = function () { | ||
return getOpenIdConfiguration(byuJwt.cache) | ||
} | ||
byuJwt.getPublicKey = function() { | ||
return init(byuJwt.cache) | ||
.then(getPublicKey) | ||
byuJwt.getPublicKey = async function () { | ||
return initPublicKey(byuJwt.cache) | ||
} | ||
byuJwt.verifyJWT = function(jwt) { | ||
return init(byuJwt.cache) | ||
.then(openIdConfig => verifyJWT(byuJwt.options, openIdConfig, jwt)) | ||
.then(() => true) | ||
.catch(() => false) | ||
byuJwt.verifyJWT = async function (jwt) { | ||
try { | ||
await verifyJWT(byuJwt.options, byuJwt.cache, jwt) | ||
return true | ||
} catch (e) { | ||
return false | ||
} | ||
} | ||
Object.defineProperties(byuJwt, { | ||
cacheTTL: { | ||
get: function() { return byuJwt.cache.getTTL() }, | ||
set: function(ttl) { byuJwt.cache.setTTL(ttl) } | ||
get: () => ({ | ||
openId: byuJwt.cache.openId.getTTL(), | ||
byuCert: byuJwt.cache.byuCert.getTTL() | ||
}), | ||
set: (ttl) => ({ | ||
openId: byuJwt.cache.openId.setTTL(ttl), | ||
byuCert: byuJwt.cache.byuCert.setTTL(ttl) | ||
}) | ||
}, | ||
openIdTTL: { | ||
get: () => byuJwt.cache.openId.getTTL(), | ||
set: (ttl) => byuJwt.cache.openId.setTTL(ttl) | ||
}, | ||
byuCertTTL: { | ||
get: () => byuJwt.cache.byuCert.getTTL(), | ||
set: (ttl) => byuJwt.cache.byuCert.setTTL(ttl) | ||
} | ||
}) | ||
@@ -113,6 +126,4 @@ | ||
Object.defineProperties(ByuJWT, { | ||
'BYU_JWT_HEADER_CURRENT': { | ||
BYU_JWT_HEADER_CURRENT: { | ||
value: BYU_JWT_CURRENT.header, | ||
@@ -122,3 +133,3 @@ writable: false | ||
'BYU_JWT_HEADER_ORIGINAL': { | ||
BYU_JWT_HEADER_ORIGINAL: { | ||
value: BYU_JWT_ORIGINAL.header, | ||
@@ -128,3 +139,3 @@ writable: false | ||
'AuthenticationError': { | ||
AuthenticationError: { | ||
value: AuthenticationError, | ||
@@ -134,3 +145,3 @@ writable: false | ||
'JsonWebTokenError': { | ||
JsonWebTokenError: { | ||
value: jsonWebToken.JsonWebTokenError, | ||
@@ -140,3 +151,3 @@ writable: false | ||
'NotBeforeError': { | ||
NotBeforeError: { | ||
value: jsonWebToken.NotBeforeError, | ||
@@ -146,3 +157,3 @@ writable: false | ||
'TokenExpiredError': { | ||
TokenExpiredError: { | ||
value: jsonWebToken.TokenExpiredError, | ||
@@ -152,3 +163,3 @@ writable: false | ||
'WELL_KNOWN_URL': { | ||
WELL_KNOWN_URL: { | ||
value: WELL_KNOWN_URL, | ||
@@ -159,52 +170,37 @@ writable: false | ||
function authenticate(options, openIdConfig, headers) { | ||
const promises = [] | ||
async function authenticate (options, cache, headers) { | ||
const verifiedJWTs = {} | ||
// scan headers for provided JWT info | ||
;[BYU_JWT_ORIGINAL, BYU_JWT_CURRENT] | ||
.forEach(data => { | ||
if (headers[data.header]) { | ||
debug('verifying JWT in header ' + data.header) | ||
const promise = decodeJWT(options, openIdConfig, headers[data.header]) | ||
.then(decodedJWT => { | ||
debug('verify JWT complete for header ' + data.header) | ||
verifiedJWTs[data.key] = decodedJWT | ||
}) | ||
.catch(err => { | ||
debug('verify JWT failed for header ' + data.header + ': ' + err.stack) | ||
const name = (data.name ? data.name + ' ' : '') | ||
const prefix = err instanceof jsonWebToken.TokenExpiredError ? 'Expired ' : 'Invalid ' | ||
throw new AuthenticationError(prefix + name + 'JWT', err) | ||
}) | ||
promises.push(promise) | ||
} else { | ||
promises.push(null) | ||
await Promise.all([BYU_JWT_ORIGINAL, BYU_JWT_CURRENT].map(async ({ header, name, key }) => { | ||
if (headers[header]) { | ||
debug('verifying JWT in header ' + header) | ||
try { | ||
const decodedJWT = await decodeJWT(options, cache, headers[header]) | ||
debug('verify JWT complete for header ' + header) | ||
verifiedJWTs[key] = decodedJWT | ||
} catch (err) { | ||
debug('verify JWT failed for header ' + header + ': ' + err.stack) | ||
const prefix = err instanceof jsonWebToken.TokenExpiredError ? 'Expired ' : 'Invalid ' | ||
throw new AuthenticationError(prefix + (name ? name + ' ' : '') + 'JWT', err) | ||
} | ||
}) | ||
} | ||
})) | ||
return Promise.all(promises) | ||
.then(() => { | ||
if (!verifiedJWTs.current) { | ||
debug('verify JWT missing expected JWT') | ||
throw new AuthenticationError('Missing expected JWT') | ||
} | ||
if (!verifiedJWTs.current) { | ||
debug('verify JWT missing expected JWT') | ||
throw new AuthenticationError('Missing expected JWT') | ||
} | ||
// extra validation step for production | ||
if (!options.development && options.basePath) { | ||
const context = verifiedJWTs.current.raw['http://wso2.org/claims/apicontext'] | ||
if (!context.startsWith(options.basePath)) throw new AuthenticationError('Invalid API context in JWT') | ||
} | ||
// extra validation step for production | ||
if (!options.development && options.basePath) { | ||
const context = verifiedJWTs.current.raw['http://wso2.org/claims/apicontext'] | ||
if (!context.startsWith(options.basePath)) throw new AuthenticationError('Invalid API context in JWT') | ||
} | ||
verifiedJWTs.originalJWT = headers[BYU_JWT_ORIGINAL.header] || headers[BYU_JWT_CURRENT.header] | ||
verifiedJWTs.claims = (verifiedJWTs.original && verifiedJWTs.original.resourceOwner) || | ||
(verifiedJWTs.current && verifiedJWTs.current.resourceOwner) || | ||
(verifiedJWTs.original && verifiedJWTs.original.client) || | ||
verifiedJWTs.current.client | ||
verifiedJWTs.originalJWT = headers[BYU_JWT_ORIGINAL.header] || headers[BYU_JWT_CURRENT.header] | ||
verifiedJWTs.claims = (verifiedJWTs.original && verifiedJWTs.original.resourceOwner) || | ||
(verifiedJWTs.current && verifiedJWTs.current.resourceOwner) || | ||
(verifiedJWTs.original && verifiedJWTs.original.client) || | ||
verifiedJWTs.current.client | ||
return verifiedJWTs | ||
}) | ||
return verifiedJWTs | ||
} | ||
@@ -215,66 +211,59 @@ | ||
* @param {object} options | ||
* @param {object} openIdConfig | ||
* @param {object} cache | ||
* @param {string} jwt | ||
* @returns {Promise.<Object>} | ||
*/ | ||
function decodeJWT(options, openIdConfig, jwt) { | ||
return verifyJWT(options, openIdConfig, jwt) | ||
.then(verifiedJWT => { | ||
const hasResourceOwner = typeof verifiedJWT['http://byu.edu/claims/resourceowner_byu_id'] !== "undefined" | ||
const result = {} | ||
result.client = { | ||
byuId: verifiedJWT['http://byu.edu/claims/client_byu_id'], | ||
claimSource: verifiedJWT['http://byu.edu/claims/client_claim_source'], | ||
netId: verifiedJWT['http://byu.edu/claims/client_net_id'], | ||
personId: verifiedJWT['http://byu.edu/claims/client_person_id'], | ||
preferredFirstName: verifiedJWT['http://byu.edu/claims/client_preferred_first_name'], | ||
prefix: verifiedJWT['http://byu.edu/claims/client_name_prefix'], | ||
restOfName: verifiedJWT['http://byu.edu/claims/client_rest_of_name'], | ||
sortName: verifiedJWT['http://byu.edu/claims/client_sort_name'], | ||
subscriberNetId: verifiedJWT['http://byu.edu/claims/client_subscriber_net_id'], | ||
suffix: verifiedJWT['http://byu.edu/claims/client_name_prefix'], | ||
surname: verifiedJWT['http://byu.edu/claims/client_surname'], | ||
surnamePosition: verifiedJWT['http://byu.edu/claims/client_surname_position'] | ||
async function decodeJWT (options, cache, jwt) { | ||
const verifiedJWT = await verifyJWT(options, cache, jwt) | ||
const hasResourceOwner = typeof verifiedJWT['http://byu.edu/claims/resourceowner_byu_id'] !== 'undefined' | ||
const result = { | ||
client: { | ||
byuId: verifiedJWT['http://byu.edu/claims/client_byu_id'], | ||
claimSource: verifiedJWT['http://byu.edu/claims/client_claim_source'], | ||
netId: verifiedJWT['http://byu.edu/claims/client_net_id'], | ||
personId: verifiedJWT['http://byu.edu/claims/client_person_id'], | ||
preferredFirstName: verifiedJWT['http://byu.edu/claims/client_preferred_first_name'], | ||
prefix: verifiedJWT['http://byu.edu/claims/client_name_prefix'], | ||
restOfName: verifiedJWT['http://byu.edu/claims/client_rest_of_name'], | ||
sortName: verifiedJWT['http://byu.edu/claims/client_sort_name'], | ||
subscriberNetId: verifiedJWT['http://byu.edu/claims/client_subscriber_net_id'], | ||
suffix: verifiedJWT['http://byu.edu/claims/client_name_prefix'], | ||
surname: verifiedJWT['http://byu.edu/claims/client_surname'], | ||
surnamePosition: verifiedJWT['http://byu.edu/claims/client_surname_position'] | ||
}, | ||
...(hasResourceOwner && { | ||
resourceOwner: { | ||
byuId: verifiedJWT['http://byu.edu/claims/resourceowner_byu_id'], | ||
netId: verifiedJWT['http://byu.edu/claims/resourceowner_net_id'], | ||
personId: verifiedJWT['http://byu.edu/claims/resourceowner_person_id'], | ||
preferredFirstName: verifiedJWT['http://byu.edu/claims/resourceowner_preferred_first_name'], | ||
prefix: verifiedJWT['http://byu.edu/claims/resourceowner_prefix'], | ||
restOfName: verifiedJWT['http://byu.edu/claims/resourceowner_rest_of_name'], | ||
sortName: verifiedJWT['http://byu.edu/claims/resourceowner_sort_name'], | ||
suffix: verifiedJWT['http://byu.edu/claims/resourceowner_suffix'], | ||
surname: verifiedJWT['http://byu.edu/claims/resourceowner_surname'], | ||
surnamePosition: verifiedJWT['http://byu.edu/claims/resourceowner_surname_position'] | ||
} | ||
if (hasResourceOwner) { | ||
result.resourceOwner = { | ||
byuId: verifiedJWT['http://byu.edu/claims/resourceowner_byu_id'], | ||
netId: verifiedJWT['http://byu.edu/claims/resourceowner_net_id'], | ||
personId: verifiedJWT['http://byu.edu/claims/resourceowner_person_id'], | ||
preferredFirstName: verifiedJWT['http://byu.edu/claims/resourceowner_preferred_first_name'], | ||
prefix: verifiedJWT['http://byu.edu/claims/resourceowner_prefix'], | ||
restOfName: verifiedJWT['http://byu.edu/claims/resourceowner_rest_of_name'], | ||
sortName: verifiedJWT['http://byu.edu/claims/resourceowner_sort_name'], | ||
suffix: verifiedJWT['http://byu.edu/claims/resourceowner_suffix'], | ||
surname: verifiedJWT['http://byu.edu/claims/resourceowner_surname'], | ||
surnamePosition: verifiedJWT['http://byu.edu/claims/resourceowner_surname_position'] | ||
} | ||
} | ||
result.claims = hasResourceOwner ? result.resourceOwner : result.client | ||
result.raw = verifiedJWT | ||
result.wso2 = { | ||
apiContext: verifiedJWT["http://wso2.org/claims/apicontext"], | ||
application: { | ||
id: verifiedJWT["http://wso2.org/claims/applicationid"], | ||
name: verifiedJWT["http://wso2.org/claims/applicationname"], | ||
tier: verifiedJWT["http://wso2.org/claims/applicationtier"] | ||
}, | ||
clientId: verifiedJWT["http://wso2.org/claims/client_id"], | ||
endUser: verifiedJWT["http://wso2.org/claims/enduser"], | ||
endUserTenantId: verifiedJWT["http://wso2.org/claims/enduserTenantId"], | ||
keyType: verifiedJWT["http://wso2.org/claims/keytype"], | ||
subscriber: verifiedJWT["http://wso2.org/claims/subscriber"], | ||
tier: verifiedJWT["http://wso2.org/claims/tier"], | ||
userType: verifiedJWT["http://wso2.org/claims/usertype"], | ||
version: verifiedJWT["http://wso2.org/claims/version"] | ||
} | ||
debug('decoded JWT') | ||
return result | ||
}) | ||
}), | ||
raw: verifiedJWT, | ||
wso2: { | ||
apiContext: verifiedJWT['http://wso2.org/claims/apicontext'], | ||
application: { | ||
id: verifiedJWT['http://wso2.org/claims/applicationid'], | ||
name: verifiedJWT['http://wso2.org/claims/applicationname'], | ||
tier: verifiedJWT['http://wso2.org/claims/applicationtier'] | ||
}, | ||
clientId: verifiedJWT['http://wso2.org/claims/client_id'], | ||
endUser: verifiedJWT['http://wso2.org/claims/enduser'], | ||
endUserTenantId: verifiedJWT['http://wso2.org/claims/enduserTenantId'], | ||
keyType: verifiedJWT['http://wso2.org/claims/keytype'], | ||
subscriber: verifiedJWT['http://wso2.org/claims/subscriber'], | ||
tier: verifiedJWT['http://wso2.org/claims/tier'], | ||
userType: verifiedJWT['http://wso2.org/claims/usertype'], | ||
version: verifiedJWT['http://wso2.org/claims/version'] | ||
} | ||
} | ||
result.claims = hasResourceOwner ? result.resourceOwner : result.client | ||
debug('decoded JWT') | ||
return result | ||
} | ||
@@ -286,11 +275,16 @@ | ||
*/ | ||
function getOpenIdConfiguration(cache) { | ||
async function getOpenIdConfiguration (cache) { | ||
debug('get OpenID configuration') | ||
const promise = request(WELL_KNOWN_URL) | ||
.catch(err => { | ||
cache.clearCache() | ||
throw err | ||
}) | ||
cache.setCache(promise) | ||
return promise | ||
try { | ||
const config = await request(WELL_KNOWN_URL) | ||
debug('OpenID configuration acquired') | ||
const maxAge = getMaxAge(config.headers) | ||
const ttl = maxAgeInMinutes(maxAge) | ||
cache.openId.setTTL(ttl) | ||
cache.openId.setCache(config.body) | ||
return config.body | ||
} catch (err) { | ||
cache.openId.clearCache() | ||
throw err | ||
} | ||
} | ||
@@ -300,31 +294,58 @@ | ||
* Get the public key for the OpenID configuration | ||
* @param {object} openIdConfig | ||
* @param {object} cache | ||
* @returns {string} | ||
*/ | ||
function getPublicKey(openIdConfig) { | ||
async function getPublicKey (cache) { | ||
debug('getting public key') | ||
return request(openIdConfig["jwks_uri"]) | ||
.then(result => { | ||
const keys = result.keys | ||
const cert = | ||
"-----BEGIN CERTIFICATE-----\n" + | ||
keys[0].x5c[0].replace(/(.{64})/g, "$1\n") + | ||
"\n-----END CERTIFICATE-----" | ||
const openIdConfig = await initOpenId(cache) | ||
try { | ||
const result = await request(openIdConfig.jwks_uri) | ||
const cert = | ||
'-----BEGIN CERTIFICATE-----\n' + | ||
result.body.keys[0].x5c[0].replace(/(.{64})/g, '$1\n') + | ||
'\n-----END CERTIFICATE-----' | ||
//extract public key | ||
return new Promise((resolve, reject) => { | ||
pem.getPublicKey(cert, (err, data) => { | ||
if (err) { | ||
debug('failed to get public key') | ||
reject(err) | ||
} else { | ||
debug('public key acquired') | ||
resolve(data.publicKey) | ||
} | ||
}) | ||
}) | ||
}) | ||
// extract public key | ||
const { publicKey } = await pemGetPublicKey(cert) | ||
debug('public key acquired') | ||
const maxAge = getMaxAge(result.headers) | ||
const ttl = maxAgeInMinutes(maxAge) | ||
cache.byuCert.setTTL(ttl) | ||
cache.byuCert.setCache(publicKey) | ||
return publicKey | ||
} catch (err) { | ||
debug('failed to get public key') | ||
cache.byuCert.clearCache() | ||
throw err | ||
} | ||
} | ||
/** | ||
* Get the max-age cache control from headers | ||
* @params {object} headers | ||
* @returns {number} | ||
*/ | ||
function getMaxAge (headers) { | ||
debug('getting max age from cache control header') | ||
const cacheControl = headers['cache-control'] | ||
let [, maxAge] = cacheControl.match(/max-age=(\d+)/) // Get digits | ||
maxAge = parseInt(maxAge, 10) | ||
if (!maxAge) { | ||
debug('defaulting max age to 3600') | ||
return 3600 | ||
} | ||
debug(`max age is ${maxAge}`) | ||
return maxAge | ||
} | ||
/** | ||
* Convert seconds to mintues | ||
* @params {number} seconds | ||
* @returns {number} | ||
*/ | ||
function maxAgeInMinutes (seconds) { | ||
return Math.floor(seconds / 60) | ||
} | ||
/** | ||
* Get cached OpenID configuration or new OpenID configuration if the cached is expired. | ||
@@ -334,15 +355,18 @@ * @param {object} cache | ||
*/ | ||
function init(cache) { | ||
return cache.getCache() || getOpenIdConfiguration(cache) | ||
function initOpenId (cache) { | ||
return cache.openId.getCache() || getOpenIdConfiguration(cache) | ||
} | ||
async function initPublicKey (cache) { | ||
return cache.byuCert.getCache() || getPublicKey(cache) | ||
} | ||
/** | ||
* Verify the JWT against the OpenID configuration. | ||
* @param {object} options | ||
* @param {object} openIdConfig | ||
* @param {object} cache | ||
* @param {string} jwt | ||
* @returns {Promise<Object>} | ||
*/ | ||
function verifyJWT(options, openIdConfig, jwt) { | ||
async function verifyJWT (options, cache, jwt) { | ||
// we can skip verification | ||
@@ -352,25 +376,19 @@ if (options.development) { | ||
debug('JWT verification skipped in development mode') | ||
return Promise.resolve(jsonWebToken.decode(jwt)) | ||
return jsonWebToken.decode(jwt) | ||
} | ||
const algorithms = openIdConfig["id_token_signing_alg_values_supported"] | ||
return getPublicKey(openIdConfig) | ||
.then(publicKey => { | ||
return new Promise(function(resolve, reject) { | ||
debug('verifying JWT') | ||
return jsonWebToken.verify(jwt, publicKey, {algorithms: algorithms}, (err, decoded) => { | ||
if (err) { | ||
if (err.name === 'TokenExpiredError') { | ||
debug('token expired at ' + err.expiredAt + ' for JWT ' + jwt) | ||
} else { | ||
debug('failed verifying JWT: ' + err.message + ' for JWT ' + jwt) | ||
} | ||
reject(err) | ||
} else { | ||
debug('verified JWT') | ||
resolve(decoded) | ||
} | ||
}) | ||
}) | ||
}) | ||
} | ||
const openIdConfig = await initOpenId(cache) | ||
const algorithms = openIdConfig.id_token_signing_alg_values_supported | ||
const publicKey = await getPublicKey(cache) | ||
debug('verifying JWT') | ||
try { | ||
const verifiedJWT = await jsonWebTokenVerify(jwt, publicKey, { algorithms }) | ||
debug('verified JWT') | ||
return verifiedJWT | ||
} catch (err) { | ||
if (err.name === 'TokenExpiredError') debug('token expired at ' + err.expiredAt + ' for JWT ' + jwt) | ||
else debug('failed verifying JWT: ' + err.message + ' for JWT ' + jwt) | ||
throw err | ||
} | ||
} |
@@ -18,5 +18,5 @@ /** | ||
'use strict' | ||
const debug = require('debug')('byu-jwt-request') | ||
const http = require('http') | ||
const https = require('https') | ||
const debug = require('debug')('byu-jwt-request') | ||
const http = require('http') | ||
const https = require('https') | ||
@@ -26,6 +26,6 @@ /** | ||
* @param {string} url | ||
* @returns {Promise<Object>} | ||
* @returns {Promise<{ body: Object, headers: Object }>} | ||
*/ | ||
module.exports = function request(url) { | ||
debug('making request to ' + url); | ||
module.exports = function request (url) { | ||
debug('making request to ' + url) | ||
return new Promise((resolve, reject) => { | ||
@@ -40,8 +40,11 @@ const mod = /^https/.test(url) ? https : http | ||
res.on('end', () => { | ||
debug('completed request to ' + url); | ||
debug('completed request to ' + url) | ||
let body | ||
const headers = res.headers | ||
try { | ||
resolve(JSON.parse(data)) | ||
body = JSON.parse(data) | ||
} catch (err) { | ||
reject(Error('Invalid response body:' + data)) | ||
} | ||
resolve({ body, headers }) | ||
}) | ||
@@ -51,6 +54,6 @@ }) | ||
req.on('error', err => { | ||
debug('failed request to ' + url); | ||
debug('failed request to ' + url) | ||
reject(err) | ||
}) | ||
}) | ||
} | ||
} |
{ | ||
"name": "byu-jwt", | ||
"version": "1.0.11", | ||
"version": "2.0.0", | ||
"description": "The byu-jwt module provides helpful functions to retrieve a specified BYU .well-known URL and verify BYU signed JWTs.", | ||
@@ -16,4 +16,8 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "mocha test" | ||
"test": "mocha test", | ||
"lint": "./node_modules/.bin/standard --fix" | ||
}, | ||
"engines": { | ||
"node": ">=8" | ||
}, | ||
"homepage": "https://github.com/byu-oit/byu-jwt-nodejs#readme", | ||
@@ -23,10 +27,11 @@ "dependencies": { | ||
"jsonwebtoken": "^8.5.1", | ||
"pem": "^1.14.2" | ||
"pem": "^1.14.3" | ||
}, | ||
"devDependencies": { | ||
"aws-sdk": "^2.472.0", | ||
"aws-sdk": "^2.578.0", | ||
"chai": "^4.2.0", | ||
"mocha": "^6.1.4", | ||
"request": "^2.88.0" | ||
"mocha": "^6.2.2", | ||
"request": "^2.88.0", | ||
"standard": "^14.3.1" | ||
} | ||
} |
@@ -5,4 +5,7 @@ # byu-jwt | ||
**Requires Node 8 or above** | ||
## Table of Contents | ||
- [Migrate from v1 to v2](#migrate-from-v1-to-v2) | ||
- [API](#api) | ||
@@ -20,2 +23,5 @@ - [Constructor](#constructor) | ||
## Migrate from v1 to v2 | ||
* Update to Node 8 or above | ||
## API | ||
@@ -22,0 +28,0 @@ |
@@ -17,8 +17,10 @@ /* | ||
*/ | ||
const AWS = require('aws-sdk') | ||
const expect = require('chai').expect | ||
const ByuJWT = require('../index') | ||
const request = require('request') | ||
/* global describe it beforeEach before */ | ||
describe('byu-jwt', function() { | ||
const AWS = require('aws-sdk') | ||
const expect = require('chai').expect | ||
const ByuJWT = require('../index') | ||
const request = require('request') | ||
describe('byu-jwt', function () { | ||
let byuJWT | ||
@@ -28,25 +30,25 @@ let jwt | ||
before(done => { | ||
console.log('Acquiring test credentials. Please wait...'); | ||
console.log('Acquiring test credentials. Please wait...') | ||
byuJWT = ByuJWT({ cacheTTL: 0 }) | ||
const ssm = new AWS.SSM({ region: 'us-west-2' }); | ||
const ssm = new AWS.SSM({ region: 'us-west-2' }) | ||
const params = { | ||
Name: 'wabs-oauth-test.dev.config', | ||
WithDecryption: true | ||
}; | ||
ssm.getParameter(params, function(err, param) { | ||
} | ||
ssm.getParameter(params, function (err, param) { | ||
if (err) { | ||
console.error('AWS Error: ' + err.message); | ||
console.error('AWS Error: ' + err.message) | ||
console.log('Make sure that you have awslogin (https://github.com/byu-oit/awslogin) ' + | ||
'installed, run the command "awslogin" in your terminal, and select the "dev-oit-byu" ' + | ||
'account.'); | ||
process.exit(1); | ||
'account.') | ||
process.exit(1) | ||
} | ||
let config; | ||
let config | ||
try { | ||
config = JSON.parse(param.Parameter.Value); | ||
config = JSON.parse(param.Parameter.Value) | ||
} catch (err) { | ||
console.error('Parameter parsing error: ' + err.message); | ||
process.exit(1); | ||
console.error('Parameter parsing error: ' + err.message) | ||
process.exit(1) | ||
} | ||
@@ -61,3 +63,4 @@ | ||
} | ||
request(reqConfig, function(err, res, body) { | ||
request(reqConfig, function (err, res, body) { | ||
if (err) throw Error('Unable to request JWT: \n' + err) | ||
try { | ||
@@ -69,5 +72,5 @@ const obj = typeof body === 'object' ? body : JSON.parse(body) | ||
} | ||
done(); | ||
done() | ||
}) | ||
}); | ||
}) | ||
}) | ||
@@ -91,3 +94,2 @@ | ||
describe('verifyJWT', () => { | ||
it('valid JWT', () => { | ||
@@ -106,7 +108,5 @@ return byuJWT.verifyJWT(jwt) | ||
}) | ||
}) | ||
describe('decodeJWT', () => { | ||
it('valid JWT', () => { | ||
@@ -126,7 +126,5 @@ return byuJWT.decodeJWT(jwt) | ||
}) | ||
}) | ||
describe('authenticate', () => { | ||
it('valid JWT', () => { | ||
@@ -150,3 +148,2 @@ const headers = {} | ||
}) | ||
}) | ||
@@ -167,7 +164,7 @@ | ||
promise: deferred.promise, | ||
status: function(code) { | ||
status: function (code) { | ||
this.code = code | ||
return res | ||
}, | ||
send: function(body) { | ||
send: function (body) { | ||
this.body = body | ||
@@ -184,3 +181,3 @@ deferred.resolve() | ||
byuJWT.authenticateUAPIMiddleware(req, res, function() { | ||
byuJWT.authenticateUAPIMiddleware(req, res, function () { | ||
expect(req).to.have.ownProperty('verifiedJWTs') | ||
@@ -195,4 +192,4 @@ done() | ||
const req = { headers: headers } | ||
byuJWT.authenticateUAPIMiddleware(req, res, function() { | ||
done(Error('should not get here')) | ||
byuJWT.authenticateUAPIMiddleware(req, res, function () { | ||
throw Error('should not get here') | ||
}) | ||
@@ -204,5 +201,3 @@ return res.promise | ||
}) | ||
}) | ||
}); | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
197
44645
5
10
681
Updatedpem@^1.14.3