diff --git a/package-lock.json b/package-lock.json index 32b3583d..79c89d93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,12 +24,12 @@ "express-session": "^1.17.2", "global-agent": "^3.0.0", "got": "^11.8.1", - "jose": "^2.0.4", + "jose": "^4.3.7", "jsonwebtoken": "^8.4.0", "memcached": "^2.2.2", "memorystore": "^1.6.6", "morgan": "^1.9.1", - "openid-client": "^4.7.5", + "openid-client": "^5.1.1", "passport": "^0.4.0", "passport-dropbox-oauth2": "^1.1.0", "passport-facebook": "^3.0.0", @@ -76,7 +76,7 @@ "standard-version": "^9.3.1" }, "engines": { - "node": ">=12.19.0 <=15.11.0" + "node": ">=12.19.0 <=16.13.0" } }, "node_modules/@babel/code-frame": { @@ -1070,14 +1070,6 @@ "uid2": "0.0.3" } }, - "node_modules/@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==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@sindresorhus/is": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", @@ -1334,6 +1326,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -1980,6 +1973,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, "engines": { "node": ">=6" } @@ -5568,6 +5562,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "engines": { "node": ">=8" } @@ -6221,15 +6216,9 @@ } }, "node_modules/jose": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", - "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", - "dependencies": { - "@panva/asn1.js": "^1.0.0" - }, - "engines": { - "node": ">=10.13.0 < 13 || >=13.7.0" - }, + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.3.7.tgz", + "integrity": "sha512-S7Xfsy8nN9Iw/AZxk+ZxEbd5ImIwJPM0TfAo8zI8FF+3lidQ2yiK4dqzsaPKSbZD0woNVSY0KCql6rlKc5V7ug==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -7042,11 +7031,6 @@ "semver": "bin/semver.js" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, "node_modules/map-obj": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", @@ -8349,20 +8333,17 @@ } }, "node_modules/openid-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.7.5.tgz", - "integrity": "sha512-9APA9gHikzzRCc9z3lmIsZ1LcRHho9uTXxt567QlVmAmS2qoVpChTOdla7US9RrbiZsIh50xXd9DpLzh68FtgQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.1.1.tgz", + "integrity": "sha512-vwbS4T7hpaWol0GerNabnslUWTxq1NHjnLqdFovzqWlLHW5kp08Tme8FSSeTswABjSC9d88ofTFnfAYy/zwtlQ==", "dependencies": { - "aggregate-error": "^3.1.0", - "got": "^11.8.0", - "jose": "^2.0.5", + "jose": "^4.1.4", "lru-cache": "^6.0.0", - "make-error": "^1.3.6", "object-hash": "^2.0.1", "oidc-token-hash": "^5.0.1" }, "engines": { - "node": "^10.19.0 || >=12.0.0 < 13 || >=13.7.0 < 14 || >= 14.2.0" + "node": "^12.19.0 || ^14.15.0 || ^16.13.0" }, "funding": { "url": "https://github.com/sponsors/panva" @@ -12764,11 +12745,6 @@ "uid2": "0.0.3" } }, - "@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==" - }, "@sindresorhus/is": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", @@ -12990,6 +12966,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, "requires": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -13493,7 +13470,8 @@ "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true }, "cli-cursor": { "version": "2.1.0", @@ -16253,7 +16231,8 @@ "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true }, "inflight": { "version": "1.0.6", @@ -16736,12 +16715,9 @@ } }, "jose": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", - "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", - "requires": { - "@panva/asn1.js": "^1.0.0" - } + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.3.7.tgz", + "integrity": "sha512-S7Xfsy8nN9Iw/AZxk+ZxEbd5ImIwJPM0TfAo8zI8FF+3lidQ2yiK4dqzsaPKSbZD0woNVSY0KCql6rlKc5V7ug==" }, "js-tokens": { "version": "4.0.0", @@ -17388,11 +17364,6 @@ } } }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, "map-obj": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", @@ -18375,15 +18346,12 @@ } }, "openid-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.7.5.tgz", - "integrity": "sha512-9APA9gHikzzRCc9z3lmIsZ1LcRHho9uTXxt567QlVmAmS2qoVpChTOdla7US9RrbiZsIh50xXd9DpLzh68FtgQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.1.1.tgz", + "integrity": "sha512-vwbS4T7hpaWol0GerNabnslUWTxq1NHjnLqdFovzqWlLHW5kp08Tme8FSSeTswABjSC9d88ofTFnfAYy/zwtlQ==", "requires": { - "aggregate-error": "^3.1.0", - "got": "^11.8.0", - "jose": "^2.0.5", + "jose": "^4.1.4", "lru-cache": "^6.0.0", - "make-error": "^1.3.6", "object-hash": "^2.0.1", "oidc-token-hash": "^5.0.1" }, diff --git a/package.json b/package.json index 41a10c4a..81f094fd 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ ] }, "engines": { - "node": ">=12.19.0 <=15.11.0" + "node": ">=12.19.0 <=16.13.0" }, "scripts": { "start": "NODE_ENV=production node server/app.js", @@ -43,12 +43,12 @@ "express-session": "^1.17.2", "global-agent": "^3.0.0", "got": "^11.8.1", - "jose": "^2.0.4", + "jose": "^4.3.7", "jsonwebtoken": "^8.4.0", "memcached": "^2.2.2", "memorystore": "^1.6.6", "morgan": "^1.9.1", - "openid-client": "^4.7.5", + "openid-client": "^5.1.1", "passport": "^0.4.0", "passport-dropbox-oauth2": "^1.1.0", "passport-facebook": "^3.0.0", diff --git a/server/utils/openid-client-helper.js b/server/utils/openid-client-helper.js index 1bca5175..af2179a6 100644 --- a/server/utils/openid-client-helper.js +++ b/server/utils/openid-client-helper.js @@ -1,4 +1,4 @@ -const { JWK: { generateSync }, JWKS: { KeyStore, asKeyStore } } = require('jose') +const jose = require('jose') const { Issuer } = require('openid-client') const path = require('path') const fs = require('fs') @@ -12,12 +12,17 @@ const clientJWKSFilePath = path.join(`${process.cwd()}/server`, 'jwks') * @returns undefined */ async function generateJWKS (provider) { - const keyType = generateSync('RSA') - const keyStore = new KeyStore(keyType) const fileName = path.join(fileUtils.makeDir(clientJWKSFilePath), provider.id + '.json') - if (!fs.existsSync(fileName)) { - await fileUtils.writeDataToFile(fileName, JSON.stringify(keyStore.toJWKS(true))) + if (fs.existsSync(fileName)) { + return } + + const { privateKey, publicKey } = await jose.generateKeyPair('RS256') + const privateJwk = await jose.exportJWK(privateKey) + const publicJwk = await jose.exportJWK(publicKey) + const kid = await jose.calculateJwkThumbprint(publicJwk) + privateJwk.kid = kid + await fileUtils.writeDataToFile(fileName, JSON.stringify({ keys: [privateJwk] })) } const clients = [] @@ -55,8 +60,7 @@ async function getClient (provider) { // generate jwks await generateJWKS(provider) const jwks = require(path.join(fileUtils.makeDir(clientJWKSFilePath), `${provider.id}.json`)) - const ks = asKeyStore(jwks) - client = new issuer.Client(options, ks.toJWKS(true)) + client = new issuer.Client(options, jwks) } else { client = new issuer.Client(options) } diff --git a/test/openid-client-helper.spec.js b/test/openid-client-helper.spec.js index d9099f13..41f4f49a 100644 --- a/test/openid-client-helper.spec.js +++ b/test/openid-client-helper.spec.js @@ -10,6 +10,7 @@ describe('Integration Test OpenID Client Helper', () => { const testProvider = passportConfigAuthorizedResponse.providers.find(p => p.id === 'oidccedev6privatejwt') let kid = null const jwksFilePath = `../server/jwks/${testProvider.id}.json` + let jwks describe('generateJWKS test', () => { const generateJWKS = rewiredOpenIDClientHelper.__get__('generateJWKS') @@ -19,16 +20,29 @@ describe('Integration Test OpenID Client Helper', () => { assert.exists(jwksFilePath, `${jwksFilePath} file not found`) }) - it('make sure jwks has keys and kid', () => { - const jwks = require(jwksFilePath) + it('jwks should have keys', () => { + jwks = require(jwksFilePath) assert.isArray(jwks.keys, 'keys not found in jwks') + }) + + it('jwks should have kid', () => { kid = jwks.keys[0].kid assert.exists(kid, 'kid not found in jwks') }) + it('jwks should have n', () => { + const n = jwks.keys[0].n + assert.exists(n, 'n not found in jwks') + }) + + it('jwks should have kty', () => { + const kty = jwks.keys[0].kty + assert.exists(kty, 'kty not found in jwks') + }) + it('make sure generateJWKS not regenerating jwks again and rewrite existing jwks data', async () => { await generateJWKS(testProvider) - const jwks = require(jwksFilePath) + jwks = require(jwksFilePath) assert.equal(kid, jwks.keys[0].kid, `${kid} is not matching with ${jwks.keys[0].kid}`) }) }) diff --git a/test/openid-client-helper.test.js b/test/openid-client-helper.test.js index 64f85e1a..65acfce3 100644 --- a/test/openid-client-helper.test.js +++ b/test/openid-client-helper.test.js @@ -5,6 +5,10 @@ const rewiredOpenIDClientHelper = rewire('../server/utils/openid-client-helper') const InitMock = require('./testdata/init-mock') const config = require('config') const nock = require('nock') +const sinon = require('sinon') +const jose = require('jose') +const { v4: uuidv4 } = require('uuid') +const fileUtils = require('../server/utils/file-utils') const assert = chai.assert const passportConfigAuthorizedResponse = config.get('passportConfigAuthorizedResponse') @@ -14,6 +18,11 @@ describe('Test OpenID Client Helper', () => { describe('generateJWKS test', () => { const generateJWKS = rewiredOpenIDClientHelper.__get__('generateJWKS') + const callGenerateJWKS = async () => { + try { + await generateJWKS({ id: uuidv4() }) + } catch (e) {} + } it('should exist', () => { assert.exists(generateJWKS) @@ -22,6 +31,44 @@ describe('Test OpenID Client Helper', () => { it('should be function', () => { assert.isFunction(generateJWKS, 'generateJWKS is not a function') }) + + it('should call fileUtils.makeDir once', async () => { + const makeDirSpy = sinon.spy(fileUtils, 'makeDir') + await callGenerateJWKS() + assert.isTrue(makeDirSpy.calledOnce) + sinon.restore() + }) + + it('should call generateKeyPair once', async () => { + const generateKeyPairSpy = sinon.spy() + sinon.stub(jose, 'generateKeyPair').value(generateKeyPairSpy) + await callGenerateJWKS() + assert.isTrue(generateKeyPairSpy.calledOnce) + sinon.restore() + }) + + it('should call exportJWK twice', async () => { + const exportJWKSpy = sinon.spy() + sinon.stub(jose, 'exportJWK').value(exportJWKSpy) + await callGenerateJWKS() + assert.isTrue(exportJWKSpy.calledTwice) + sinon.restore() + }) + + it('should call calculateJwkThumbprint once', async () => { + const calculateJwkThumbprintSpy = sinon.spy() + sinon.stub(jose, 'calculateJwkThumbprint').value(calculateJwkThumbprintSpy) + await callGenerateJWKS() + assert.isTrue(calculateJwkThumbprintSpy.calledOnce) + sinon.restore() + }) + + it('should call fileUtils.writeDataToFile once', async () => { + const writeDataToFileSpy = sinon.spy(fileUtils, 'writeDataToFile') + await callGenerateJWKS() + assert.isTrue(writeDataToFileSpy.calledOnce) + sinon.restore() + }) }) describe('getIssuer test', () => {