diff --git a/.circleci/config.yml b/.circleci/config.yml index a47d9d4..768f3e2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,9 +8,9 @@ jobs: parameters: node-version: type: string - default: "12" + default: "18" docker: - - image: circleci/node:<< parameters.node-version >> + - image: cimg/node:<< parameters.node-version >> environment: LANG: en_US.UTF-8 steps: @@ -37,7 +37,7 @@ workflows: - build: matrix: parameters: - node-version: ["10", "12", "14"] + node-version: ["14.20", "16.18", "18.12"] - ship/node-publish: requires: - build diff --git a/package-lock.json b/package-lock.json index 15d4c66..d76c646 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@types/express": "^4.17.14", "@types/jsonwebtoken": "^8.5.9", "debug": "^4.3.4", - "jose": "^2.0.6", + "jose": "^4.10.3", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" }, @@ -28,6 +28,7 @@ "express": "^4.17.1", "express-jwt": "^6.0.0", "express-jwt-v7": "npm:express-jwt@^7.5.0", + "jose2": "npm:jose@^2.0.6", "jsonwebtoken": "^8.5.1", "koa": "^2.12.1", "koa-jwt": "^3.6.0", @@ -649,6 +650,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "dev": true, "engines": { "node": ">=10.13.0" } @@ -2925,9 +2927,19 @@ } }, "node_modules/jose": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.3.tgz", + "integrity": "sha512-3S4wQnaoJKSAx9uHSoyf8B/lxjs1qCntHWL6wNFszJazo+FtWe+qD0zVfY0BlqJ5HHK4jcnM98k3BQzVLbzE4g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jose2": { + "name": "jose", "version": "2.0.6", "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "dev": true, "dependencies": { "@panva/asn1.js": "^1.0.0" }, @@ -5951,7 +5963,8 @@ "@panva/asn1.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", - "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "dev": true }, "@types/body-parser": { "version": "1.19.2", @@ -7788,9 +7801,15 @@ } }, "jose": { - "version": "2.0.6", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.3.tgz", + "integrity": "sha512-3S4wQnaoJKSAx9uHSoyf8B/lxjs1qCntHWL6wNFszJazo+FtWe+qD0zVfY0BlqJ5HHK4jcnM98k3BQzVLbzE4g==" + }, + "jose2": { + "version": "npm:jose@2.0.6", "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "dev": true, "requires": { "@panva/asn1.js": "^1.0.0" } diff --git a/package.json b/package.json index 4169c2d..4f41547 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@types/express": "^4.17.14", "@types/jsonwebtoken": "^8.5.9", "debug": "^4.3.4", - "jose": "^2.0.6", + "jose": "^4.10.3", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" }, @@ -32,6 +32,7 @@ "express-jwt": "^6.0.0", "express-jwt-v7": "npm:express-jwt@^7.5.0", "jsonwebtoken": "^8.5.1", + "jose2": "npm:jose@^2.0.6", "koa": "^2.12.1", "koa-jwt": "^3.6.0", "mocha": "^6.2.3", diff --git a/src/JwksClient.js b/src/JwksClient.js index eef29fc..80f290a 100644 --- a/src/JwksClient.js +++ b/src/JwksClient.js @@ -56,7 +56,7 @@ class JwksClient { throw new JwksError('The JWKS endpoint did not contain any keys'); } - const signingKeys = retrieveSigningKeys(keys); + const signingKeys = await retrieveSigningKeys(keys); if (!signingKeys.length) { throw new JwksError('The JWKS endpoint did not contain any signing keys'); diff --git a/src/integrations/passport.js b/src/integrations/passport.js index a3981e1..79d75da 100644 --- a/src/integrations/passport.js +++ b/src/integrations/passport.js @@ -1,4 +1,4 @@ -const JWT = require('jose').JWT; +const jose = require('jose'); const { ArgumentError } = require('../errors'); const { JwksClient } = require('../JwksClient'); const supportedAlg = require('./config'); @@ -30,7 +30,10 @@ module.exports.passportJwtSecret = function (options) { return function secretProvider(req, rawJwtToken, cb) { let decoded; try { - decoded = JWT.decode(rawJwtToken, { complete: true }); + decoded = { + payload: jose.decodeJwt(rawJwtToken), + header: jose.decodeProtectedHeader(rawJwtToken) + }; } catch (err) { decoded = null; } diff --git a/src/utils.js b/src/utils.js index 6b06404..3e51533 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,17 +1,36 @@ const jose = require('jose'); +const crypto = require('crypto'); -function retrieveSigningKeys(keys) { - const keystore = jose.JWKS.asKeyStore({ keys }, { ignoreErrors: true }); +async function retrieveSigningKeys(jwks) { + const results = []; - return keystore.all({ use: 'sig' }).map((key) => { - return { - kid: key.kid, - alg: key.alg, - get publicKey() { return key.toPEM(false); }, - get rsaPublicKey() { return key.toPEM(false); }, - getPublicKey() { return key.toPEM(false); } - }; - }); + jwks = jwks + .filter(({ use }) => use === 'sig' || use === undefined) + .filter(({ kty }) => kty === 'RSA' || kty === 'EC' || kty === 'OKP'); + + for (const jwk of jwks) { + try { + // The algorithm is actually not used in the Node.js KeyObject-based runtime + // passing an arbitrary value here and checking that KeyObject was returned + // later + const keyObject = await jose.importJWK(jwk, 'RS256'); + if (!(keyObject instanceof crypto.KeyObject) || keyObject.type !== 'public') { + continue; + } + const getSpki = () => keyObject.export({ format: 'pem', type: 'spki' }); + results.push({ + get publicKey() { return getSpki(); }, + get rsaPublicKey() { return getSpki(); }, + getPublicKey() { return getSpki(); }, + ...(typeof jwk.kid === 'string' && jwk.kid ? { kid: jwk.kid } : undefined), + ...(typeof jwk.alg === 'string' && jwk.alg ? { alg: jwk.alg } : undefined) + }); + } catch (err) { + continue; + } + } + + return results; } module.exports = { diff --git a/src/wrappers/interceptor.js b/src/wrappers/interceptor.js index 5f0eb1d..219b0e8 100644 --- a/src/wrappers/interceptor.js +++ b/src/wrappers/interceptor.js @@ -12,7 +12,7 @@ function getKeysInterceptor(client, { getKeysInterceptor }) { let signingKeys; if (keys && keys.length) { - signingKeys = retrieveSigningKeys(keys); + signingKeys = await retrieveSigningKeys(keys); } if (signingKeys && signingKeys.length) { diff --git a/tests/mocks/jwks.js b/tests/mocks/jwks.js index 90fa406..94cd5d5 100644 --- a/tests/mocks/jwks.js +++ b/tests/mocks/jwks.js @@ -1,12 +1,12 @@ const nock = require('nock'); -const jose = require('jose'); +const jose2 = require('jose2'); function jwksEndpoint(host, certs) { return nock(host) .get('/.well-known/jwks.json') .reply(200, { keys: certs.map(cert => { - const parsed = jose.JWK.asKey(cert.pub).toJWK(); + const parsed = jose2.JWK.asKey(cert.pub).toJWK(); return { ...parsed, use: 'sig',