diff --git a/ghost/core/core/server/services/auth/session/index.js b/ghost/core/core/server/services/auth/session/index.js index a7427b3108..d225854ca7 100644 --- a/ghost/core/core/server/services/auth/session/index.js +++ b/ghost/core/core/server/services/auth/session/index.js @@ -4,6 +4,7 @@ const sessionFromToken = require('@tryghost/mw-session-from-token'); const createSessionMiddleware = require('./middleware'); const settingsCache = require('../../../../shared/settings-cache'); const {GhostMailer} = require('../../mail'); +const {t} = require('../../i18n'); const expressSession = require('./express-session'); @@ -38,10 +39,12 @@ const sessionService = createSessionService({ findUserById({id}) { return models.User.findOne({id, status: 'active'}); }, - getSecret(key) { + getSettingsCache(key) { return settingsCache.get(key); }, - mailer + mailer, + urlUtils, + t }); module.exports = createSessionMiddleware({sessionService}); diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap index c23ac6dfef..3d86cbea3c 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap @@ -30,7 +30,6 @@ Object { "members": true, "newEmailAddresses": true, "outboundLinkTagging": true, - "staff2fa": true, "stripeAutomaticTax": true, "themeErrorsNotification": true, "urlCache": true, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap index 1b6a4e31cc..1bbe95e736 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/session.test.js.snap @@ -22,6 +22,9 @@ Object { "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "set-cookie": Array [ + StringMatching /\\^ghost-admin-api-session=/, + ], "vary": "Accept-Version, Origin", "x-powered-by": "Express", } diff --git a/ghost/core/test/utils/fixture-utils.js b/ghost/core/test/utils/fixture-utils.js index 6fd982f6e9..432bd80c8a 100644 --- a/ghost/core/test/utils/fixture-utils.js +++ b/ghost/core/test/utils/fixture-utils.js @@ -713,7 +713,10 @@ const fixtures = { }, async enableAllLabsFeatures() { - const labsValue = Object.fromEntries(labsService.WRITABLE_KEYS_ALLOWLIST.map(key => [key, true])); + const labsValue = Object.fromEntries(labsService.WRITABLE_KEYS_ALLOWLIST + // TODO: should test with 2fa enabled + .filter(key => key !== 'staff2fa') + .map(key => [key, true])); const labsSetting = DataGenerator.forKnex.createSetting({ key: 'labs', group: 'labs', diff --git a/ghost/session-service/lib/session-service.js b/ghost/session-service/lib/session-service.js index c04b3e10f9..04852f12d9 100644 --- a/ghost/session-service/lib/session-service.js +++ b/ghost/session-service/lib/session-service.js @@ -1,8 +1,9 @@ const { BadRequestError } = require('@tryghost/errors'); -const {totp} = require('otplib'); +const emailTemplate = require('../lib/emails/signin'); +const {totp} = require('otplib'); totp.options = { digits: 6, step: 60, @@ -12,6 +13,7 @@ totp.options = { /** * @typedef {object} User * @prop {string} id + * @prop {(attr: string) => string} get */ /** @@ -45,9 +47,10 @@ totp.options = { * @param {(req: Req, res: Res) => Promise} deps.getSession * @param {(data: {id: string}) => Promise} deps.findUserById * @param {(req: Req) => string} deps.getOriginOfRequest - * @param {(key: string) => string} deps.getSecret + * @param {(key: string) => string} deps.getSettingsCache * @param {import('../../core/core/server/services/mail').GhostMailer} deps.mailer - * + * @param {import('../../core/core/server/services/i18n').t} deps.t + * @param {import('../../core/core/shared/url-utils')} deps.urlUtils * @returns {SessionService} */ @@ -55,8 +58,10 @@ module.exports = function createSessionService({ getSession, findUserById, getOriginOfRequest, - getSecret, - mailer + getSettingsCache, + mailer, + urlUtils, + t }) { /** * cookieCsrfProtection @@ -112,8 +117,8 @@ module.exports = function createSessionService({ * @returns {Promise} */ async function generateAuthCodeForUser(req, res) { - const session = await getSession(req, res); // Todo: Do we need to handle "No session found"? - const secret = getSecret('admin_session_secret') + session.user_id; + const session = await getSession(req, res); + const secret = getSettingsCache('admin_session_secret') + session.user_id; const token = totp.generate(secret); return token; } @@ -126,8 +131,8 @@ module.exports = function createSessionService({ * @returns {Promise} */ async function verifyAuthCodeForUser(req, res) { - const session = await getSession(req, res); // Todo: Do we need to handle "No session found"? - const secret = getSecret('admin_session_secret') + session.user_id; + const session = await getSession(req, res); + const secret = getSettingsCache('admin_session_secret') + session.user_id; const isValid = totp.check(req.body.token, secret); return isValid; } @@ -140,19 +145,30 @@ module.exports = function createSessionService({ * @returns {Promise} */ async function sendAuthCodeToUser(req, res) { - const session = await getSession(req, res); // eslint-disable-line const token = await generateAuthCodeForUser(req, res); + const user = await getUserForSession(req, res); + if(!user) { + throw new BadRequestError({ + message: 'Could not fetch user from the session.' + }); + } + const recipient = user.get('email'); + const siteTitle = getSettingsCache('title'); + const siteUrl = urlUtils.urlFor('home', true); + const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i')); + const siteDomain = (domain && domain[1]); + const email = emailTemplate({ + t, + siteTitle: siteTitle, + email: recipient, + siteDomain: siteDomain, + siteUrl: siteUrl, + token + }); - // TODO: Find email address for user associated with user requesting token - const recipient = 'TODO'; - - // TODO: Generate email - const email = `

Here is your token matey: ${token}

`; - - // TODO: Send email await mailer.send({ to: recipient, - subject: 'tokens4u', + subject: `Verification code: ${token}`, html: email }); diff --git a/ghost/session-service/test/SessionService.test.js b/ghost/session-service/test/SessionService.test.js index 45680b9751..3e821e8a66 100644 --- a/ghost/session-service/test/SessionService.test.js +++ b/ghost/session-service/test/SessionService.test.js @@ -199,13 +199,13 @@ describe('SessionService', function () { }; const findUserById = sinon.spy(async ({id}) => ({id})); const getOriginOfRequest = sinon.stub().returns('origin'); - const getSecret = sinon.stub().returns('secret-key'); + const getSettingsCache = sinon.stub().returns('secret-key'); const sessionService = SessionService({ getSession, findUserById, getOriginOfRequest, - getSecret + getSettingsCache }); const req = Object.create(express.request, { @@ -227,8 +227,12 @@ describe('SessionService', function () { const authCode = await sessionService.generateAuthCodeForUser(req, res); should.exist(authCode); + req.body = { + token: authCode + }; + // Verify the auth code - const isValid = await sessionService.verifyAuthCodeForUser(req, res, authCode); + const isValid = await sessionService.verifyAuthCodeForUser(req, res); should.equal(isValid, true); }); @@ -245,13 +249,13 @@ describe('SessionService', function () { }; const findUserById = sinon.spy(async ({id}) => ({id})); const getOriginOfRequest = sinon.stub().returns('origin'); - const getSecret = sinon.stub().returns('secret-key'); + const getSettingsCache = sinon.stub().returns('secret-key'); const sessionService = SessionService({ getSession, findUserById, getOriginOfRequest, - getSecret + getSettingsCache }); const req = Object.create(express.request, { @@ -273,8 +277,12 @@ describe('SessionService', function () { const authCode = await sessionService.generateAuthCodeForUser(req, res); should.exist(authCode); + req.body = { + token: 'wrong-code' + }; + // Verify an incorrect auth code - const isValid = await sessionService.verifyAuthCodeForUser(req, res, 'wrong-code'); + const isValid = await sessionService.verifyAuthCodeForUser(req, res); should.equal(isValid, false); }); @@ -313,7 +321,7 @@ describe('SessionService', function () { getSession, findUserById, getOriginOfRequest, - getSecret: getSecretFirst + getSettingsCache: getSecretFirst }); const authCodeFirst = await sessionServiceFirst.generateAuthCodeForUser(req, res); @@ -324,7 +332,7 @@ describe('SessionService', function () { getSession, findUserById, getOriginOfRequest, - getSecret: getSecretSecond + getSettingsCache: getSecretSecond }); const authCodeSecond = await sessionServiceSecond.generateAuthCodeForUser(req, res);