From 41103000d2cb0cbfa33937972498e79d898d4c3f Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 10 May 2022 16:08:54 +0800 Subject: [PATCH] Added support for Admin API key extraction refs https://github.com/TryGhost/Toolbox/issues/292 - Allows to detect and extract admin api key ID value. The reason why we are not dealing withe the "secret" value here in a similar way as Content API key is to keep the package independent from the model layer. It only provides "identification" information along with the key type so that the version mismatch data service can deal with this information in an optimal way (just one db query). --- ghost/extract-api-key/lib/extract-api-key.js | 45 ++++++++++++++++++- ghost/extract-api-key/package.json | 4 +- .../test/extract-api-key.test.js | 28 +++++++++++- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/ghost/extract-api-key/lib/extract-api-key.js b/ghost/extract-api-key/lib/extract-api-key.js index 29c1dc0996..fd04df608a 100644 --- a/ghost/extract-api-key/lib/extract-api-key.js +++ b/ghost/extract-api-key/lib/extract-api-key.js @@ -1,16 +1,57 @@ +const jwt = require('jsonwebtoken'); + /** + * Remove 'Ghost' from raw authorization header and extract the JWT token. + * Eg. Authorization: Ghost ${JWT} + * @param {string} header + */ +const extractTokenFromHeader = (header) => { + const [scheme, token] = header.split(' '); + + if (/^Ghost$/i.test(scheme)) { + return token; + } +}; + +const extractAdminAPIKey = (token) => { + const decoded = jwt.decode(token, {complete: true}); + + if (!decoded || !decoded.header || !decoded.header.kid) { + return null; + } + + return decoded.header.kid; +}; + +/** + * @typedef {object} ApiKey + * @prop {string} key + * @prop {string} type + */ + +/** + * When it's a Content API the function resolves with the value of the key secret. + * When it's an Admin API the function resolves with the value of the key id. * * @param {import('express').Request} req - * @returns {string} + * @returns {ApiKey} */ const extractAPIKey = (req) => { let keyValue = null; + let keyType = null; if (req.query && req.query.key) { keyValue = req.query.key; + keyType = 'content'; + } else if (req.headers && req.headers.authorization) { + keyValue = extractAdminAPIKey(extractTokenFromHeader(req.headers.authorization)); + keyType = 'admin'; } - return keyValue; + return { + key: keyValue, + type: keyType + }; }; module.exports = extractAPIKey; diff --git a/ghost/extract-api-key/package.json b/ghost/extract-api-key/package.json index 86742a27e2..8ea180d53d 100644 --- a/ghost/extract-api-key/package.json +++ b/ghost/extract-api-key/package.json @@ -21,5 +21,7 @@ "access": "public" }, "devDependencies": {}, - "dependencies": {} + "dependencies": { + "jsonwebtoken": "^8.5.1" + } } diff --git a/ghost/extract-api-key/test/extract-api-key.test.js b/ghost/extract-api-key/test/extract-api-key.test.js index dab7583dbe..25d6371341 100644 --- a/ghost/extract-api-key/test/extract-api-key.test.js +++ b/ghost/extract-api-key/test/extract-api-key.test.js @@ -3,22 +3,46 @@ const extractApiKey = require('../index'); describe('Extract API Key', function () { it('Returns nulls for a request without any key', function () { - const key = extractApiKey({ + const {key, type} = extractApiKey({ query: { filter: 'status:active' } }); assert.equal(key, null); + assert.equal(type, null); }); it('Extracts Content API key from the request', function () { - const key = extractApiKey({ + const {key, type} = extractApiKey({ query: { key: '123thekey' } }); assert.equal(key, '123thekey'); + assert.equal(type, 'content'); + }); + + it('Extracts Admin API key from the request', function () { + const {key, type} = extractApiKey({ + headers: { + authorization: 'Ghost eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjYyNzM4MjQzNDZiZjUxZjNhYWI5OTA5OSJ9.eyJpYXQiOjE2NTIxNjUyNDQsImV4cCI6MTY1MjE2NTU0NCwiYXVkIjoiL3YyL2FkbWluLyJ9.VdPOZ4XffgYd8qn_46zlJR3jW_rPZTw70COkG5IYIuU' + } + }); + + assert.equal(key, '6273824346bf51f3aab99099'); + assert.equal(type, 'admin'); + }); + + it('Returns null if malformatted Admin API Key', function () { + const {key, type} = extractApiKey({ + headers: { + authorization: 'Ghost incorrectformat' + } + }); + + assert.equal(key, null); + assert.equal(type, 'admin'); }); });