UNPKG

multivocal

Version:

A node.js library to assist with building best practice, configuration driven, Actions for the Google Assistant.

218 lines (195 loc) 6.29 kB
const Jwt = require('jsonwebtoken'); const jwkToPem = require('jwk-to-pem'); const objectAssignDeep = require('object-assign-deep'); const Multivocal = require('./multivocal'); const Util = require('./util'); const Template = require('./template'); var sources = {}; /** * * @param iss The iss value to match for the source * @param source Information about this source including: * aud - The aud value to match (string or array) * Keys - An array of keys to expect or * KeysUrl - The URL containing the valid keys * * The source will get additional information added to it (including the Keys * if KeysUrl was specified and possible cache information about the keys). */ var addSource = function( iss, source ){ // Check if we have a current source var currentSource = sources[iss]; if( !currentSource ){ currentSource = { iss: iss }; } // If we have a current source, and the KeysUrl is different, reset the expiration if( currentSource.KeysUrl !== source.KeysUrl ){ delete currentSource.Expires; } // Make a copy of the data and save it objectAssignDeep( currentSource, source ); sources[iss] = currentSource; }; var addSources = function( env, sourcesList ){ if( !Array.isArray( sourcesList ) ){ sourcesList = [sourcesList]; } sourcesList.forEach( sourceName => { var sourceObject = Util.setting( env, `JWTAuth/${sourceName}` ); if( sourceObject ){ sourceObject = Template.evalObj( sourceObject, env ); var issList = sourceObject.iss; if( issList ){ if( !Array.isArray( issList ) ){ issList = [issList]; } issList.forEach( iss => { addSource( iss, sourceObject ) }) } } }) }; exports.addSources = addSources; var updateSourceKeys = function( source, env ){ var request = require('axios'); var options = { url: source.KeysUrl, resolveWithFullResponse: true }; return Multivocal.timingBegin( env, "Auth updateSourceKeys" ) .then( env => request( options ) ) .then( response => { var body = response.data; var expiresStr = response.headers.expires; var expiresDate = Date.parse( expiresStr ); var expires = expiresDate.valueOf(); var now = Date.now(); console.log('updateSourceKeys',expiresStr, expires, now); if( (!source.Keys) || now > expires ){ console.log('updating keys'); source.Keys = body.keys; source.Expires = expires; } Multivocal.timingEnd( env, "Auth updateSourceKeys" ); return Promise.resolve( source ); }); }; var possiblyUpdateSourceKeys = function( source, env ){ var now = Date.now(); console.log('possiblyUpdateSourceKeys', source.Expires, now); if( (!source.Keys) || now > source.Expires ){ return updateSourceKeys( source, env ); } else { return Promise.resolve( source ); } }; var getUnverifiedInfoSourceRaw = function( unverified ){ // Locate a source based on the issuing name var iss = unverified.payload.iss; var source = sources[iss]; if( !source ){ return Promise.reject(new Error(`No source matching "${iss}"`)); } return Promise.resolve( source ); }; var getUnverifiedInfoSource = function( unverified, env ){ return getUnverifiedInfoSourceRaw( unverified ) .then( source => possiblyUpdateSourceKeys( source, env ) ); }; var getUnverifiedInfoKey = function( unverified, source ){ // Locate a key based on the key id var kid = unverified.header.kid; var key; for( var co=0; co<source.Keys.length && (typeof key === "undefined"); co++ ){ var potentialKey = source.Keys[co]; if( potentialKey.kid === kid ){ key = potentialKey; } } if( !key ){ return Promise.reject(new Error(`No key matching "${kid}" for source "${iss}"`)); } return Promise.resolve( key ); }; var getUnverifiedInfoVerification = function( unverified, source ){ // If set, verify that the client auditing string for this source matches var audOk = false; var sourceAud = source.aud; if( sourceAud ){ if( !Array.isArray( sourceAud ) ){ sourceAud = [sourceAud]; } var claimAud = unverified.payload.aud; if( sourceAud.indexOf( claimAud ) === -1 ){ return Promise.reject(new Error(`Claimed aud "${claimAud}" does not match aud for source "${source.iss}" (${source.aud})`)); } audOk = true; } return Promise.resolve( audOk ); }; var getUnverifiedInfo = function( token, env ){ if( token.startsWith("Bearer ") ){ token = token.substring( "Bearer ".length ); } var unverified = Jwt.decode( token, {complete:true} ); var source; var key; var pem; var audOk = false; return Multivocal.timingBegin( env, "Auth getUnverifiedInfo" ) .then( env => getUnverifiedInfoSource( unverified, env ) ) .then( sourceResult => { source = sourceResult; return getUnverifiedInfoKey( unverified, source ); }) .then( keyResult => { key = keyResult; pem = jwkToPem( key ); return getUnverifiedInfoVerification( unverified, source ); }) .then( audOkResult => { audOk = audOkResult; // We're good! Return the info var ret = { iss: unverified.payload.iss, key: key, pem: pem, aud: unverified.payload.aud, audOk: audOk, token: token, unverifiedJwt: unverified }; Multivocal.timingEnd( env, "Auth getUnverifiedInfo" ); return Promise.resolve( ret ); }); }; var verifyInfo = function( info, env ){ //Multivocal.timingBegin( env, 'Auth verifyInfo' ); var token = info.token; var key = info.pem; var options = { }; return new Promise((resolve,reject) => { Jwt.verify( token, key, options, function( err, decoded ){ //Multivocal.timingEnd( env, 'Auth verifyInfo' ); if( err ){ reject( err ); } else { resolve( decoded ); } }) }); }; var verify = function( token, env ){ return getUnverifiedInfo( token, env ) .then( info => verifyInfo( info, env ) ) .then( decoded => Promise.resolve( decoded ) ) .catch( err => { console.error('Problem verifying',err); return Promise.reject( err ); }); }; exports.verify = verify;