2020-04-08 20:01:06 +00:00
"use strict" ;
2019-02-25 22:54:08 +00:00
var keyfetch = module . exports ;
2021-10-21 00:12:06 +00:00
var request = require ( "@root/request" ) . defaults ( {
userAgent : "keyfetch/v2.1.0"
} ) ;
2020-04-08 20:01:06 +00:00
var Rasha = require ( "rasha" ) ;
var Eckles = require ( "eckles" ) ;
2019-02-25 22:54:08 +00:00
var mincache = 1 * 60 * 60 ;
var maxcache = 3 * 24 * 60 * 60 ;
var staletime = 15 * 60 ;
var keyCache = { } ;
2021-10-21 00:12:06 +00:00
var Errors = require ( "./lib/errors.js" ) ;
async function requestAsync ( req ) {
var resp = await request ( req ) . catch ( Errors . BAD _GATEWAY ) ;
// differentiate potentially temporary server errors from 404
if ( ! resp . ok && ( resp . statusCode >= 500 || resp . statusCode < 200 ) ) {
2021-10-21 19:21:41 +00:00
throw Errors . BAD _GATEWAY ( { response : resp } ) ;
2021-10-21 00:12:06 +00:00
}
return resp ;
}
2019-02-25 22:54:08 +00:00
function checkMinDefaultMax ( opts , key , n , d , x ) {
2020-04-08 20:01:06 +00:00
var i = opts [ key ] ;
if ( ! i && 0 !== i ) {
return d ;
}
if ( i >= n && i >= x ) {
return parseInt ( i , 10 ) ;
} else {
2021-10-21 00:12:06 +00:00
throw Errors . DEVELOPER _ERROR ( "opts." + key + " should be at least " + n + " and at most " + x + ", not " + i ) ;
2020-04-08 20:01:06 +00:00
}
2019-02-25 22:54:08 +00:00
}
2021-10-21 19:21:41 +00:00
keyfetch . _errors = Errors ;
2019-02-25 23:17:26 +00:00
keyfetch . _clear = function ( ) {
2020-04-08 20:01:06 +00:00
keyCache = { } ;
2019-02-25 23:17:26 +00:00
} ;
2019-02-25 22:54:08 +00:00
keyfetch . init = function ( opts ) {
2020-04-08 20:01:06 +00:00
mincache = checkMinDefaultMax ( opts , "mincache" , 1 * 60 , mincache , 31 * 24 * 60 * 60 ) ;
maxcache = checkMinDefaultMax ( opts , "maxcache" , 1 * 60 * 60 , maxcache , 31 * 24 * 60 * 60 ) ;
staletime = checkMinDefaultMax ( opts , "staletime" , 1 * 60 , staletime , 31 * 24 * 60 * 60 ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . _oidc = async function ( iss ) {
2021-10-21 19:21:41 +00:00
var url = normalizeIss ( iss ) + "/.well-known/openid-configuration" ;
2021-06-15 23:22:38 +00:00
var resp = await requestAsync ( {
2021-10-21 19:21:41 +00:00
url : url ,
2021-06-15 23:22:38 +00:00
json : true
2019-02-25 22:54:08 +00:00
} ) ;
2021-10-21 00:12:06 +00:00
2021-06-15 23:22:38 +00:00
var oidcConf = resp . body ;
if ( ! oidcConf . jwks _uri ) {
2021-10-21 19:21:41 +00:00
throw Errors . NO _JWKS _URI ( url ) ;
2021-06-15 23:22:38 +00:00
}
return oidcConf ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . _wellKnownJwks = async function ( iss ) {
return keyfetch . _jwks ( normalizeIss ( iss ) + "/.well-known/jwks.json" ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . _jwks = async function ( iss ) {
var resp = await requestAsync ( { url : iss , json : true } ) ;
2021-10-21 00:12:06 +00:00
2021-06-15 23:22:38 +00:00
return Promise . all (
resp . body . keys . map ( async function ( jwk ) {
// EC keys have an x values, whereas RSA keys do not
var Keypairs = jwk . x ? Eckles : Rasha ;
var thumbprint = await Keypairs . thumbprint ( { jwk : jwk } ) ;
var pem = await Keypairs . export ( { jwk : jwk } ) ;
var cacheable = {
jwk : jwk ,
thumbprint : thumbprint ,
pem : pem
} ;
return cacheable ;
} )
) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . jwks = async function ( jwkUrl ) {
2020-04-08 20:01:06 +00:00
// TODO DRY up a bit
2021-06-15 23:22:38 +00:00
var results = await keyfetch . _jwks ( jwkUrl ) ;
await Promise . all (
results . map ( async function ( result ) {
return keyfetch . _setCache ( result . jwk . iss || jwkUrl , result ) ;
} )
) ;
// cacheable -> hit (keep original externally immutable)
return JSON . parse ( JSON . stringify ( results ) ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . wellKnownJwks = async function ( iss ) {
2020-04-08 20:01:06 +00:00
// TODO DRY up a bit
2021-06-15 23:22:38 +00:00
var results = await keyfetch . _wellKnownJwks ( iss ) ;
await Promise . all (
results . map ( async function ( result ) {
return keyfetch . _setCache ( result . jwk . iss || iss , result ) ;
} )
) ;
// result -> hit (keep original externally immutable)
return JSON . parse ( JSON . stringify ( results ) ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . oidcJwks = async function ( iss ) {
var oidcConf = await keyfetch . _oidc ( iss ) ;
// TODO DRY up a bit
var results = await keyfetch . _jwks ( oidcConf . jwks _uri ) ;
await Promise . all (
results . map ( async function ( result ) {
return keyfetch . _setCache ( result . jwk . iss || iss , result ) ;
} )
) ;
// result -> hit (keep original externally immutable)
return JSON . parse ( JSON . stringify ( results ) ) ;
2019-02-25 22:54:08 +00:00
} ;
2019-02-25 22:58:02 +00:00
function checkId ( id ) {
2020-04-08 20:01:06 +00:00
return function ( results ) {
var result = results . filter ( function ( result ) {
// we already checked iss above
return result . jwk . kid === id || result . thumbprint === id ;
} ) [ 0 ] ;
2019-02-25 22:58:02 +00:00
2020-04-08 20:01:06 +00:00
if ( ! result ) {
2021-10-21 00:12:06 +00:00
throw Errors . JWK _NOT _FOUND ( id ) ;
2020-04-08 20:01:06 +00:00
}
return result ;
} ;
2019-02-25 22:58:02 +00:00
}
2021-06-15 23:22:38 +00:00
keyfetch . oidcJwk = async function ( id , iss ) {
var hit = await keyfetch . _checkCache ( id , iss ) ;
if ( hit ) {
return hit ;
}
return keyfetch . oidcJwks ( iss ) . then ( checkId ( id ) ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . wellKnownJwk = async function ( id , iss ) {
var hit = await keyfetch . _checkCache ( id , iss ) ;
if ( hit ) {
return hit ;
}
return keyfetch . wellKnownJwks ( iss ) . then ( checkId ( id ) ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . jwk = async function ( id , jwksUrl ) {
var hit = await keyfetch . _checkCache ( id , jwksUrl ) ;
if ( hit ) {
return hit ;
}
return keyfetch . jwks ( jwksUrl ) . then ( checkId ( id ) ) ;
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . _checkCache = async function ( id , iss ) {
// We cache by thumbprint and (kid + '@' + iss),
// so it's safe to check without appending the issuer
var hit = keyCache [ id ] ;
if ( ! hit ) {
hit = keyCache [ id + "@" + normalizeIss ( iss ) ] ;
}
if ( ! hit ) {
2020-04-08 20:01:06 +00:00
return null ;
2021-06-15 23:22:38 +00:00
}
var now = Math . round ( Date . now ( ) / 1000 ) ;
var left = hit . expiresAt - now ;
// not guarding number checks since we know that we
// set 'now' and 'expiresAt' correctly elsewhere
if ( left > staletime ) {
return JSON . parse ( JSON . stringify ( hit ) ) ;
}
if ( left > 0 ) {
return JSON . parse ( JSON . stringify ( hit ) ) ;
}
return null ;
2019-02-25 22:54:08 +00:00
} ;
keyfetch . _setCache = function ( iss , cacheable ) {
2020-04-08 20:01:06 +00:00
// force into a number
var expiresAt = parseInt ( cacheable . jwk . exp , 10 ) || 0 ;
var now = Date . now ( ) / 1000 ;
var left = expiresAt - now ;
2019-02-25 22:54:08 +00:00
2020-04-08 20:01:06 +00:00
// TODO maybe log out when any of these non-ideal cases happen?
if ( ! left ) {
expiresAt = now + maxcache ;
} else if ( left < mincache ) {
expiresAt = now + mincache ;
} else if ( left > maxcache ) {
expiresAt = now + maxcache ;
}
2019-02-25 22:54:08 +00:00
2020-04-08 20:01:06 +00:00
// cacheable = { jwk, thumprint, pem }
cacheable . createdAt = now ;
cacheable . expiresAt = expiresAt ;
keyCache [ cacheable . thumbprint ] = cacheable ;
keyCache [ cacheable . jwk . kid + "@" + normalizeIss ( iss ) ] = cacheable ;
2019-02-25 22:54:08 +00:00
} ;
function normalizeIss ( iss ) {
2021-06-15 23:03:18 +00:00
if ( ! iss ) {
2021-10-21 19:21:41 +00:00
throw Errors . NO _ISSUER ( ) ;
2021-06-15 23:03:18 +00:00
}
2020-04-08 20:01:06 +00:00
// We definitely don't want false negatives stemming
// from https://example.com vs https://example.com/
// We also don't want to allow insecure issuers
if ( /^http:/ . test ( iss ) && ! process . env . KEYFETCH _ALLOW _INSECURE _HTTP ) {
// note, we wrap some things in promises just so we can throw here
2021-10-21 00:12:06 +00:00
throw Errors . INSECURE _ISSUER ( iss ) ;
2020-04-08 20:01:06 +00:00
}
return iss . replace ( /\/$/ , "" ) ;
2019-02-25 22:54:08 +00:00
}
2019-03-15 19:45:27 +00:00
keyfetch . jwt = { } ;
keyfetch . jwt . decode = function ( jwt ) {
2021-10-21 00:12:06 +00:00
try {
var parts = jwt . split ( "." ) ;
// JWS
var obj = { protected : parts [ 0 ] , payload : parts [ 1 ] , signature : parts [ 2 ] } ;
// JWT
obj . header = JSON . parse ( Buffer . from ( obj . protected , "base64" ) ) ;
obj . claims = JSON . parse ( Buffer . from ( obj . payload , "base64" ) ) ;
return obj ;
} catch ( e ) {
2021-10-21 19:21:41 +00:00
var err = Errors . PARSE _ERROR ( jwt ) ;
2021-10-21 00:12:06 +00:00
err . details = e . message ;
throw err ;
}
2019-02-25 22:54:08 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . jwt . verify = async function ( jwt , opts ) {
2020-04-08 20:01:06 +00:00
if ( ! opts ) {
opts = { } ;
2019-03-09 09:50:14 +00:00
}
2021-10-21 19:21:41 +00:00
var jws ;
2021-06-15 23:22:38 +00:00
var exp ;
var nbf ;
var active ;
2021-10-21 00:12:06 +00:00
var now ;
var then ;
2021-06-15 23:22:38 +00:00
var issuers = opts . issuers || [ ] ;
if ( opts . iss ) {
issuers . push ( opts . iss ) ;
}
if ( opts . claims && opts . claims . iss ) {
issuers . push ( opts . claims . iss ) ;
}
if ( ! issuers . length ) {
2021-10-21 00:12:06 +00:00
if ( ! ( opts . jwk || opts . jwks ) ) {
throw Errors . DEVELOPER _ERROR (
"[keyfetch.js] Security Error: Neither of opts.issuers nor opts.iss were provided. If you would like to bypass issuer verification (i.e. for federated authn) you must explicitly set opts.issuers = ['*']. Otherwise set a value such as https://accounts.google.com/"
) ;
}
2021-06-15 23:22:38 +00:00
}
var claims = opts . claims || { } ;
if ( ! jwt || "string" === typeof jwt ) {
2021-10-21 19:21:41 +00:00
jws = keyfetch . jwt . decode ( jwt ) ;
2021-06-15 23:22:38 +00:00
} else {
2021-10-21 19:21:41 +00:00
jws = jwt ;
2021-06-15 23:22:38 +00:00
}
2019-03-09 01:00:41 +00:00
2021-10-21 19:21:41 +00:00
if ( ! jws . claims . iss || ! issuers . some ( isTrustedIssuer ( jws . claims . iss ) ) ) {
2021-10-21 00:12:06 +00:00
if ( ! ( opts . jwk || opts . jwks ) ) {
2021-10-21 19:21:41 +00:00
throw Errors . UNKNOWN _ISSUER ( jws . claims . iss || "" ) ;
2021-10-21 00:12:06 +00:00
}
2021-06-15 23:22:38 +00:00
}
// Note claims.iss validates more strictly than opts.issuers (requires exact match)
2021-10-21 19:21:41 +00:00
var failedClaims = Object . keys ( claims )
. filter ( function ( key ) {
if ( claims [ key ] !== jws . claims [ key ] ) {
2021-06-15 23:22:38 +00:00
return true ;
2020-04-08 20:01:06 +00:00
}
2021-06-15 23:22:38 +00:00
} )
2021-10-21 19:21:41 +00:00
. map ( function ( key ) {
return "jwt.claims." + key + " = " + JSON . stringify ( jws . claims [ key ] ) ;
} ) ;
if ( failedClaims . length ) {
throw Errors . FAILED _CLAIMS ( failedClaims , Object . keys ( claims ) ) ;
2021-06-15 23:22:38 +00:00
}
2021-10-21 19:21:41 +00:00
exp = jws . claims . exp ;
2021-10-21 00:12:06 +00:00
if ( exp && false !== opts . exp ) {
now = Date . now ( ) ;
// TODO document that opts.exp can be used as leeway? Or introduce opts.leeway?
2021-10-21 19:21:41 +00:00
// fair, but not necessary
exp = parseInt ( exp , 10 ) ;
if ( isNaN ( exp ) ) {
throw Errors . MALFORMED _EXP ( JSON . stringify ( jws . claims . exp ) ) ;
}
2021-10-21 00:12:06 +00:00
then = ( opts . exp || 0 ) + parseInt ( exp , 10 ) ;
active = then - now / 1000 > 0 ;
2021-06-15 23:22:38 +00:00
// expiration was on the token or, if not, such a token is not allowed
2021-10-21 00:12:06 +00:00
if ( ! active ) {
2021-10-21 19:21:41 +00:00
throw Errors . EXPIRED ( exp ) ;
2020-04-08 20:01:06 +00:00
}
2021-06-15 23:22:38 +00:00
}
2021-10-21 00:12:06 +00:00
2021-10-21 19:21:41 +00:00
nbf = jws . claims . nbf ;
2021-06-15 23:22:38 +00:00
if ( nbf ) {
active = parseInt ( nbf , 10 ) - Date . now ( ) / 1000 <= 0 ;
if ( ! active ) {
2021-10-21 19:21:41 +00:00
throw Errors . INACTIVE ( nbf ) ;
2020-04-08 20:01:06 +00:00
}
2021-06-15 23:22:38 +00:00
}
if ( opts . jwks || opts . jwk ) {
return overrideLookup ( opts . jwks || [ opts . jwk ] ) ;
}
2019-03-09 01:00:41 +00:00
2021-10-21 19:21:41 +00:00
var kid = jws . header . kid ;
2021-06-15 23:22:38 +00:00
var iss ;
var fetcher ;
var fetchOne ;
if ( ! opts . strategy || "oidc" === opts . strategy ) {
2021-10-21 19:21:41 +00:00
iss = jws . claims . iss ;
2021-06-15 23:22:38 +00:00
fetcher = keyfetch . oidcJwks ;
fetchOne = keyfetch . oidcJwk ;
} else if ( "auth0" === opts . strategy || "well-known" === opts . strategy ) {
2021-10-21 19:21:41 +00:00
iss = jws . claims . iss ;
2021-06-15 23:22:38 +00:00
fetcher = keyfetch . wellKnownJwks ;
fetchOne = keyfetch . wellKnownJwk ;
} else {
iss = opts . strategy ;
fetcher = keyfetch . jwks ;
fetchOne = keyfetch . jwk ;
}
2019-02-27 06:11:26 +00:00
2021-06-15 23:22:38 +00:00
if ( kid ) {
return fetchOne ( kid , iss ) . then ( verifyOne ) ; //.catch(fetchAny);
}
return fetcher ( iss ) . then ( verifyAny ) ;
2019-03-15 19:45:27 +00:00
2021-06-15 23:22:38 +00:00
function verifyOne ( hit ) {
2021-10-21 19:21:41 +00:00
if ( true === keyfetch . jws . verify ( jws , hit ) ) {
return jws ;
2020-04-08 20:01:06 +00:00
}
2021-10-21 19:21:41 +00:00
throw Errors . BAD _SIGNATURE ( jws . protected + "." + jws . payload + "." + jws . signature ) ;
2021-06-15 23:22:38 +00:00
}
2020-04-08 20:01:06 +00:00
2021-06-15 23:22:38 +00:00
function verifyAny ( hits ) {
if (
hits . some ( function ( hit ) {
if ( kid ) {
if ( kid !== hit . jwk . kid && kid !== hit . thumbprint ) {
return ;
2020-04-08 20:01:06 +00:00
}
2021-10-21 19:21:41 +00:00
if ( true === keyfetch . jws . verify ( jws , hit ) ) {
2021-06-15 23:22:38 +00:00
return true ;
}
2021-10-21 19:21:41 +00:00
throw Errors . BAD _SIGNATURE ( ) ;
}
if ( true === keyfetch . jws . verify ( jws , hit ) ) {
return true ;
2021-06-15 23:22:38 +00:00
}
} )
) {
2021-10-21 19:21:41 +00:00
return jws ;
2020-04-08 20:01:06 +00:00
}
2021-10-21 19:21:41 +00:00
throw Errors . JWK _NOT _FOUND _OLD ( kid ) ;
2021-06-15 23:22:38 +00:00
}
2020-04-08 20:01:06 +00:00
2021-06-15 23:22:38 +00:00
function overrideLookup ( jwks ) {
return Promise . all (
jwks . map ( async function ( jwk ) {
var Keypairs = jwk . x ? Eckles : Rasha ;
var pem = await Keypairs . export ( { jwk : jwk } ) ;
var thumb = await Keypairs . thumbprint ( { jwk : jwk } ) ;
return { jwk : jwk , pem : pem , thumbprint : thumb } ;
} )
) . then ( verifyAny ) ;
}
2019-03-15 19:45:27 +00:00
} ;
keyfetch . jws = { } ;
keyfetch . jws . verify = function ( jws , pub ) {
2020-04-08 20:01:06 +00:00
var alg = "SHA" + jws . header . alg . replace ( /[^\d]+/i , "" ) ;
var sig = ecdsaJoseSigToAsn1Sig ( jws . header , jws . signature ) ;
return require ( "crypto" )
. createVerify ( alg )
. update ( jws . protected + "." + jws . payload )
. verify ( pub . pem , sig , "base64" ) ;
2019-03-15 19:45:27 +00:00
} ;
// old, gotta make sure nothing else uses this
keyfetch . _decode = function ( jwt ) {
2020-04-08 20:01:06 +00:00
var obj = keyfetch . jwt . decode ( jwt ) ;
return { header : obj . header , payload : obj . claims , signature : obj . signature } ;
2019-03-15 19:45:27 +00:00
} ;
2021-06-15 23:22:38 +00:00
keyfetch . verify = async function ( opts ) {
2020-04-08 20:01:06 +00:00
var jwt = opts . jwt ;
2021-06-15 23:22:38 +00:00
var obj = await keyfetch . jwt . verify ( jwt , opts ) ;
return { header : obj . header , payload : obj . claims , signature : obj . signature } ;
2019-02-25 22:54:08 +00:00
} ;
2019-03-09 09:50:14 +00:00
2019-05-06 09:06:49 +00:00
function ecdsaJoseSigToAsn1Sig ( header , b64sig ) {
2020-04-08 20:01:06 +00:00
// ECDSA JWT signatures differ from "normal" ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
if ( ! /^ES/i . test ( header . alg ) ) {
return b64sig ;
}
2019-03-09 09:50:14 +00:00
2020-04-08 20:01:06 +00:00
var bufsig = Buffer . from ( b64sig , "base64" ) ;
var hlen = bufsig . byteLength / 2 ; // should be even
var r = bufsig . slice ( 0 , hlen ) ;
var s = bufsig . slice ( hlen ) ;
// unpad positive ints less than 32 bytes wide
while ( ! r [ 0 ] ) {
r = r . slice ( 1 ) ;
}
while ( ! s [ 0 ] ) {
s = s . slice ( 1 ) ;
}
// pad (or re-pad) ambiguously non-negative BigInts to 33 bytes wide
if ( 0x80 & r [ 0 ] ) {
r = Buffer . concat ( [ Buffer . from ( [ 0 ] ) , r ] ) ;
}
if ( 0x80 & s [ 0 ] ) {
s = Buffer . concat ( [ Buffer . from ( [ 0 ] ) , s ] ) ;
}
2019-03-09 09:50:14 +00:00
2020-04-08 20:01:06 +00:00
var len = 2 + r . byteLength + 2 + s . byteLength ;
var head = [ 0x30 ] ;
// hard code 0x80 + 1 because it won't be longer than
// two SHA512 plus two pad bytes (130 bytes <= 256)
if ( len >= 0x80 ) {
head . push ( 0x81 ) ;
}
head . push ( len ) ;
2019-03-09 09:50:14 +00:00
2020-04-08 20:01:06 +00:00
var buf = Buffer . concat ( [
Buffer . from ( head ) ,
Buffer . from ( [ 0x02 , r . byteLength ] ) ,
r ,
Buffer . from ( [ 0x02 , s . byteLength ] ) ,
s
] ) ;
2019-03-09 09:50:14 +00:00
2020-04-08 20:01:06 +00:00
return buf . toString ( "base64" ) . replace ( /-/g , "+" ) . replace ( /_/g , "/" ) . replace ( /=/g , "" ) ;
2019-03-09 09:50:14 +00:00
}
2019-03-15 19:45:27 +00:00
function isTrustedIssuer ( issuer ) {
2020-04-08 20:01:06 +00:00
return function ( trusted ) {
if ( "*" === trusted ) {
return true ;
}
// TODO account for '*.example.com'
trusted = /^http(s?):\/\// . test ( trusted ) ? trusted : "https://" + trusted ;
return issuer . replace ( /\/$/ , "" ) === trusted . replace ( /\/$/ , "" ) && trusted ;
} ;
2019-03-15 19:45:27 +00:00
}