From d246a4761e46ae45f318d70112cdaff1535ab21b Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Mon, 20 Jan 2020 13:45:58 +0200 Subject: [PATCH] Implemented externally verifiable identity tokens no-issue This adds two new endpoints, one at /ghost/.well-known/jwks.json for exposing a public key, and one on the canary api /identities, which allows the Owner user to fetch a JWT. This token can then be used by external services to verify the domain * Added ghost_{public,private}_key settings This key can be used for generating tokens for communicating with external services on behalf of Ghost * Added .well-known directory to /ghost/.well-known We add a jwks.json file to the .well-known directory which exposes a public JWK which can be used to verify the signatures of JWT's created by Ghost This is added to the /ghost/ path so that it can live on the admin domain, rather than the frontend. This is because most of its uses/functions will be in relation to the admin domain. * Improved settings model tests This removes hardcoded positions in favour of testing that a particular event wasn't emitted which is less brittle and more precise about what's being tested * Fixed parent app unit tests for well-known This updates the parent app unit tests to check that the well-known route is mounted. We all change proxyquire to use `noCallThru` which ensures that the ubderlying modules are not required. This stops the initialisation logic in ./well-known erroring in tests https://github.com/thlorenz/proxyquire/issues/215 * Moved jwt signature to a separate 'token' propery This structure corresponds to other resources and allows to exptend with additional properties in future if needed --- core/server/api/canary/identities.js | 36 ++++++ core/server/api/canary/index.js | 4 + core/server/api/canary/utils/permissions.js | 8 +- .../utils/serializers/output/identities.js | 7 ++ .../canary/utils/serializers/output/index.js | 4 + core/server/data/schema/default-settings.json | 6 + .../server/data/schema/fixtures/fixtures.json | 5 + core/server/models/settings.js | 14 ++- core/server/web/api/canary/admin/routes.js | 3 + core/server/web/parent-app.js | 1 + core/server/web/well-known.js | 23 ++++ .../api/canary/admin/identities_spec.js | 103 +++++++++++++++++ core/test/unit/data/schema/integrity_spec.js | 2 +- core/test/unit/models/settings_spec.js | 9 +- core/test/unit/web/parent-app_spec.js | 7 +- package.json | 1 + yarn.lock | 105 +++++++++++++++++- 17 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 core/server/api/canary/identities.js create mode 100644 core/server/api/canary/utils/serializers/output/identities.js create mode 100644 core/server/web/well-known.js create mode 100644 core/test/regression/api/canary/admin/identities_spec.js diff --git a/core/server/api/canary/identities.js b/core/server/api/canary/identities.js new file mode 100644 index 0000000000..2d2f63d4d6 --- /dev/null +++ b/core/server/api/canary/identities.js @@ -0,0 +1,36 @@ +const settings = require('../../services/settings/cache'); +const urlUtils = require('../../lib/url-utils'); +const jwt = require('jsonwebtoken'); +const jose = require('node-jose'); +const issuer = urlUtils.urlFor('admin', true); + +const dangerousPrivateKey = settings.get('ghost_private_key'); +const keyStore = jose.JWK.createKeyStore(); +const keyStoreReady = keyStore.add(dangerousPrivateKey, 'pem'); + +const getKeyID = async () => { + const key = await keyStoreReady; + return key.kid; +}; + +const sign = async (claims, options) => { + const kid = await getKeyID(); + return jwt.sign(claims, dangerousPrivateKey, Object.assign({ + issuer, + expiresIn: '5m', + algorithm: 'RS256', + keyid: kid + }, options)); +}; + +module.exports = { + docName: 'identities', + permissions: true, + read: { + permissions: true, + async query(frame) { + const token = await sign({sub: frame.user.get('email')}); + return {token}; + } + } +}; diff --git a/core/server/api/canary/index.js b/core/server/api/canary/index.js index 8de45184cd..cc7d5bf4fc 100644 --- a/core/server/api/canary/index.js +++ b/core/server/api/canary/index.js @@ -14,6 +14,10 @@ module.exports = { return shared.pipeline(require('./db'), localUtils); }, + get identities() { + return shared.pipeline(require('./identities'), localUtils); + }, + get integrations() { return shared.pipeline(require('./integrations'), localUtils); }, diff --git a/core/server/api/canary/utils/permissions.js b/core/server/api/canary/utils/permissions.js index c704f20e43..449263a4c9 100644 --- a/core/server/api/canary/utils/permissions.js +++ b/core/server/api/canary/utils/permissions.js @@ -14,8 +14,12 @@ const common = require('../../../lib/common'); const nonePublicAuth = (apiConfig, frame) => { debug('check admin permissions'); - const singular = apiConfig.docName.replace(/s$/, ''); - + let singular; + if (apiConfig.docName.match(/ies$/)) { + singular = apiConfig.docName.replace(/ies$/, 'y'); + } else { + singular = apiConfig.docName.replace(/s$/, ''); + } let permissionIdentifier = frame.options.id; // CASE: Target ctrl can override the identifier. The identifier is the unique identifier of the target resource diff --git a/core/server/api/canary/utils/serializers/output/identities.js b/core/server/api/canary/utils/serializers/output/identities.js new file mode 100644 index 0000000000..593381fa12 --- /dev/null +++ b/core/server/api/canary/utils/serializers/output/identities.js @@ -0,0 +1,7 @@ +module.exports = { + read(data, apiConfig, frame) { + frame.response = { + identities: [data] + }; + } +}; diff --git a/core/server/api/canary/utils/serializers/output/index.js b/core/server/api/canary/utils/serializers/output/index.js index 4039e3b5bd..9b83bc7a14 100644 --- a/core/server/api/canary/utils/serializers/output/index.js +++ b/core/server/api/canary/utils/serializers/output/index.js @@ -67,6 +67,10 @@ module.exports = { return require('./member-signin_urls'); }, + get identities() { + return require('./identities'); + }, + get images() { return require('./images'); }, diff --git a/core/server/data/schema/default-settings.json b/core/server/data/schema/default-settings.json index 6153a67901..bcfd1a1d20 100644 --- a/core/server/data/schema/default-settings.json +++ b/core/server/data/schema/default-settings.json @@ -14,6 +14,12 @@ }, "theme_session_secret": { "defaultValue": null + }, + "ghost_public_key": { + "defaultValue": null + }, + "ghost_private_key": { + "defaultValue": null } }, "blog": { diff --git a/core/server/data/schema/fixtures/fixtures.json b/core/server/data/schema/fixtures/fixtures.json index fc09d9e31f..aa59e77b64 100644 --- a/core/server/data/schema/fixtures/fixtures.json +++ b/core/server/data/schema/fixtures/fixtures.json @@ -417,6 +417,11 @@ "name": "Read member signin urls", "action_type": "read", "object_type": "member_signin_url" + }, + { + "name": "Read identities", + "action_type": "read", + "object_type": "identity" } ] }, diff --git a/core/server/models/settings.js b/core/server/models/settings.js index ef8fae67ed..936e47b294 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -23,6 +23,16 @@ const getMembersKey = doBlock(() => { }; }); +const getGhostKey = doBlock(() => { + let UNO_KEYPAIRINO; + return function getGhostKey(type) { + if (!UNO_KEYPAIRINO) { + UNO_KEYPAIRINO = keypair({bits: 1024}); + } + return UNO_KEYPAIRINO[type]; + }; +}); + // For neatness, the defaults file is split into categories. // It's much easier for us to work with it as a single level // instead of iterating those categories every time @@ -38,7 +48,9 @@ function parseDefaultSettings() { theme_session_secret: () => crypto.randomBytes(32).toString('hex'), members_public_key: () => getMembersKey('public'), members_private_key: () => getMembersKey('private'), - members_email_auth_secret: () => crypto.randomBytes(64).toString('hex') + members_email_auth_secret: () => crypto.randomBytes(64).toString('hex'), + ghost_public_key: () => getGhostKey('public'), + ghost_private_key: () => getGhostKey('private') }; _.each(defaultSettingsInCategories, function each(settings, categoryName) { diff --git a/core/server/web/api/canary/admin/routes.js b/core/server/web/api/canary/admin/routes.js index 1563a2fa7a..86dcce1988 100644 --- a/core/server/web/api/canary/admin/routes.js +++ b/core/server/web/api/canary/admin/routes.js @@ -177,6 +177,9 @@ module.exports = function apiRoutes() { ); router.del('/session', mw.authAdminApi, http(apiCanary.session.delete)); + // ## Identity + router.get('/identities', mw.authAdminApi, http(apiCanary.identities.read)); + // ## Authentication router.post('/authentication/passwordreset', shared.middlewares.brute.globalReset, diff --git a/core/server/web/parent-app.js b/core/server/web/parent-app.js index 0180c87838..677bcba6eb 100644 --- a/core/server/web/parent-app.js +++ b/core/server/web/parent-app.js @@ -58,6 +58,7 @@ module.exports = function setupParentApp(options = {}) { adminApp.use(sentry.requestHandler); adminApp.enable('trust proxy'); // required to respect x-forwarded-proto in admin requests adminApp.use('/ghost/api', require('./api')()); + adminApp.use('/ghost/.well-known', require('./well-known')()); adminApp.use('/ghost', require('./admin')()); // TODO: remove {admin url}/content/* once we're sure the API is not returning relative asset URLs anywhere diff --git a/core/server/web/well-known.js b/core/server/web/well-known.js new file mode 100644 index 0000000000..beeadb41a5 --- /dev/null +++ b/core/server/web/well-known.js @@ -0,0 +1,23 @@ +const express = require('express'); +const settings = require('../services/settings/cache'); +const jose = require('node-jose'); + +const dangerousPrivateKey = settings.get('ghost_private_key'); +const keyStore = jose.JWK.createKeyStore(); +const keyStoreReady = keyStore.add(dangerousPrivateKey, 'pem'); + +const getSafePublicJWKS = async () => { + await keyStoreReady; + return keyStore.toJSON(); +}; + +module.exports = function setupWellKnownApp() { + const wellKnownApp = express(); + + wellKnownApp.get('/jwks.json', async (req, res) => { + const jwks = await getSafePublicJWKS(); + res.json(jwks); + }); + + return wellKnownApp; +}; diff --git a/core/test/regression/api/canary/admin/identities_spec.js b/core/test/regression/api/canary/admin/identities_spec.js new file mode 100644 index 0000000000..92350492d2 --- /dev/null +++ b/core/test/regression/api/canary/admin/identities_spec.js @@ -0,0 +1,103 @@ +const should = require('should'); +const supertest = require('supertest'); +const jwt = require('jsonwebtoken'); +const jwksClient = require('jwks-rsa'); +const testUtils = require('../../../../utils'); +const localUtils = require('./utils'); +const config = require('../../../../../server/config'); + +const ghost = testUtils.startGhost; + +let request; + +const verifyJWKS = (endpoint, token) => { + return new Promise((resolve, reject) => { + const jwksClient = require('jwks-rsa'); + const client = jwksClient({ + jwksUri: endpoint + }); + + function getKey(header, callback){ + client.getSigningKey(header.kid, (err, key) => { + let signingKey = key.publicKey || key.rsaPublicKey; + callback(null, signingKey); + }); + } + + jwt.verify(token, getKey, {}, (err, decoded) => { + if (err) { + reject(err); + } + + resolve(decoded); + }); + }); +}; + +describe('Identities API', function () { + describe('As Owner', function () { + before(function () { + return ghost() + .then(function () { + request = supertest.agent(config.get('url')); + }) + .then(function () { + return localUtils.doAuth(request); + }); + }); + + it('Can create JWT token and verify it afterwards with public jwks', function () { + let identity; + + return request + .get(localUtils.API.getApiQuery(`identities/`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .then((res) => { + should.not.exist(res.headers['x-cache-invalidate']); + const jsonResponse = res.body; + should.exist(jsonResponse); + should.exist(jsonResponse.identities); + + identity = jsonResponse.identities[0]; + }) + .then(() => { + return verifyJWKS(`${request.app}/ghost/.well-known/jwks.json`, identity.token); + }) + .then((decoded) => { + decoded.sub.should.equal('jbloggs@example.com'); + }); + }); + }); + + describe('As non-Owner', function () { + before(function () { + return ghost() + .then(function (_ghostServer) { + request = supertest.agent(config.get('url')); + }) + .then(function () { + return testUtils.createUser({ + user: testUtils.DataGenerator.forKnex.createUser({email: 'admin+1@ghost.org'}), + role: testUtils.DataGenerator.Content.roles[0].name + }); + }) + .then(function (admin) { + request.user = admin; + + return localUtils.doAuth(request); + }); + }); + + it('Cannot read', function () { + return request + .get(localUtils.API.getApiQuery(`identities/`)) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(403); + }); + }); +}); diff --git a/core/test/unit/data/schema/integrity_spec.js b/core/test/unit/data/schema/integrity_spec.js index 91e33a186b..119fbf2850 100644 --- a/core/test/unit/data/schema/integrity_spec.js +++ b/core/test/unit/data/schema/integrity_spec.js @@ -20,7 +20,7 @@ var should = require('should'), describe('DB version integrity', function () { // Only these variables should need updating const currentSchemaHash = '7cd198f085844aa5725964069b051189'; - const currentFixturesHash = 'b2e26827d712513907054782a0be5735'; + const currentFixturesHash = '1e5856f5172a4389bd72a98b388792e6'; // If this test is failing, then it is likely a change has been made that requires a DB version bump, // and the values above will need updating as confirmation diff --git a/core/test/unit/models/settings_spec.js b/core/test/unit/models/settings_spec.js index e080df92e6..d672ab730d 100644 --- a/core/test/unit/models/settings_spec.js +++ b/core/test/unit/models/settings_spec.js @@ -114,7 +114,7 @@ describe('Unit: models/settings', function () { return models.Settings.populateDefaults() .then(() => { // 2 events per item - settings.added and settings.[name].added - eventSpy.callCount.should.equal(88); + eventSpy.callCount.should.equal(92); const eventsEmitted = eventSpy.args.map(args => args[0]); const checkEventEmitted = event => should.ok(eventsEmitted.includes(event), `${event} event should be emitted`); @@ -136,10 +136,9 @@ describe('Unit: models/settings', function () { return models.Settings.populateDefaults() .then(() => { - // 2 events per item - settings.added and settings.[name].added - eventSpy.callCount.should.equal(86); - - eventSpy.args[13][0].should.equal('settings.logo.added'); + const eventsEmitted = eventSpy.args.map(args => args[0]); + const checkEventNotEmitted = event => should.ok(!eventsEmitted.includes(event), `${event} event should be emitted`); + checkEventNotEmitted('settings.description.added'); }); }); }); diff --git a/core/test/unit/web/parent-app_spec.js b/core/test/unit/web/parent-app_spec.js index 19a950e8fb..a7687dfb8a 100644 --- a/core/test/unit/web/parent-app_spec.js +++ b/core/test/unit/web/parent-app_spec.js @@ -1,6 +1,6 @@ const should = require('should'); const sinon = require('sinon'); -const proxyquire = require('proxyquire'); +const proxyquire = require('proxyquire').noCallThru(); const configUtils = require('../../utils/configUtils'); describe('parent app', function () { @@ -10,6 +10,7 @@ describe('parent app', function () { let apiSpy; let parentApp; let adminSpy; + let wellKnownSpy; let siteSpy; let gatewaySpy; let authPagesSpy; @@ -24,6 +25,7 @@ describe('parent app', function () { vhostSpy = sinon.spy(); apiSpy = sinon.spy(); adminSpy = sinon.spy(); + wellKnownSpy = sinon.spy(); siteSpy = sinon.spy(); gatewaySpy = sinon.spy(); authPagesSpy = sinon.spy(); @@ -33,6 +35,7 @@ describe('parent app', function () { '@tryghost/vhost-middleware': vhostSpy, './api': apiSpy, './admin': adminSpy, + './well-known': wellKnownSpy, './site': siteSpy, '../services/members': { gateway: gatewaySpy, @@ -54,10 +57,12 @@ describe('parent app', function () { parentApp(); use.calledWith('/ghost/api').should.be.true(); + use.calledWith('/ghost/.well-known').should.be.true(); use.calledWith('/ghost').should.be.true(); use.calledWith('/content/images').should.be.false(); apiSpy.called.should.be.true(); + wellKnownSpy.called.should.be.true(); adminSpy.called.should.be.true(); siteSpy.called.should.be.true(); diff --git a/package.json b/package.json index e70f4e37ae..1b0fec9182 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "grunt-shell": "3.0.1", "grunt-subgrunt": "1.3.0", "grunt-update-submodules": "0.4.1", + "jwks-rsa": "1.7.0", "matchdep": "2.0.0", "mocha": "7.1.0", "mock-knex": "0.4.7", diff --git a/yarn.lock b/yarn.lock index c90d480ae9..0a4a4d322e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -416,6 +416,14 @@ resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.27.tgz#61eb4d75dc6bfbce51cf49ee9bbebe941b2cb5d0" integrity sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ== +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" @@ -431,6 +439,45 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-jwt@0.0.42": + version "0.0.42" + resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz#4f04e1fadf9d18725950dc041808a4a4adf7f5ae" + integrity sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag== + dependencies: + "@types/express" "*" + "@types/express-unless" "*" + +"@types/express-serve-static-core@*": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf" + integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express-unless@*": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.1.tgz#4f440b905e42bbf53382b8207bc337dc5ff9fd1f" + integrity sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw== + dependencies: + "@types/express" "*" + +"@types/express@*": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c" + integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + "@types/http-cache-semantics@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" @@ -443,11 +490,21 @@ dependencies: "@types/node" "*" +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + "@types/node@*": version "12.7.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + "@types/responselike@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -455,6 +512,14 @@ dependencies: "@types/node" "*" +"@types/serve-static@*": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" + integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -4804,6 +4869,19 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwks-rsa@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.7.0.tgz#5f3362bb428d72a5f40719b5e10785fa094d1a01" + integrity sha512-tq7DVJt9J6wTvl9+AQfwZIiPSuY2Vf0F+MovfRTFuBqLB1xgDVhegD33ChEAQ6yBv9zFvUIyj4aiwrSA5VehUw== + dependencies: + "@types/express-jwt" "0.0.42" + debug "^4.1.0" + jsonwebtoken "^8.5.1" + limiter "^1.1.4" + lru-memoizer "^2.0.1" + ms "^2.1.2" + request "^2.88.0" + jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" @@ -4998,6 +5076,11 @@ liftoff@~2.5.0: rechoir "^0.6.2" resolve "^1.1.7" +limiter@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + linkify-it@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" @@ -5251,6 +5334,22 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@~4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + integrity sha1-HRdnnAac2l0ECZGgnbwsDbN35V4= + dependencies: + pseudomap "^1.0.1" + yallist "^2.0.0" + +lru-memoizer@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.1.0.tgz#2551b29d5181188695d30f12f6a56fad5b418b61" + integrity sha512-oKjxgJhL+m1wfEkez7/a6iyRZUdohej+2u04qCaAQ7BBfx/qD4RH3jOQhPsd8Y3pcm7IhcNtE3kCEIDCMPiJFQ== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "~4.0.0" + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -5784,7 +5883,7 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@^2.0.0, ms@^2.1.1: +ms@^2.0.0, ms@^2.1.1, ms@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -7036,7 +7135,7 @@ proxyquire@2.1.3: module-not-found-error "^1.0.1" resolve "^1.11.1" -pseudomap@^1.0.2: +pseudomap@^1.0.1, pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= @@ -9214,7 +9313,7 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== -yallist@^2.1.2: +yallist@^2.0.0, yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=