diff --git a/core/server/services/members/SingleUseTokenProvider.js b/core/server/services/members/SingleUseTokenProvider.js new file mode 100644 index 0000000000..f7bb694c30 --- /dev/null +++ b/core/server/services/members/SingleUseTokenProvider.js @@ -0,0 +1,67 @@ +// @ts-check +const {UnauthorizedError} = require('@tryghost/errors'); + +class SingleUseTokenProvider { + /** + * @param {import('../../models/base')} SingleUseTokenModel - A model for creating and retrieving tokens. + * @param {number} validity - How long a token is valid for from it's creation in milliseconds. + */ + constructor(SingleUseTokenModel, validity) { + this.model = SingleUseTokenModel; + this.validity = validity; + } + + /** + * @method create + * Creates and stores a token, with the passed data associated with it. + * Returns the created token value. + * + * @param {Object} data + * + * @returns {Promise} + */ + async create(data) { + const model = await this.model.add({ + data: JSON.stringify(data) + }); + + return model.get('token'); + } + + /** + * @method validate + * Validates a token, returning any parsable data associated. + * If the token is invalid the returned Promise will reject. + * + * @param {string} token + * + * @returns {Promise>} + */ + async validate(token) { + const model = await this.model.findOne({token}); + + if (!model) { + throw new UnauthorizedError({ + message: 'Invalid token provided' + }); + } + + const createdAtEpoch = model.get('created_at').getTime(); + + const tokenLifetimeMilliseconds = Date.now() - createdAtEpoch; + + if (tokenLifetimeMilliseconds > this.validity) { + throw new UnauthorizedError({ + message: 'Token expired' + }); + } + + try { + return JSON.parse(model.get('data')); + } catch (err) { + return {}; + } + } +} + +module.exports = SingleUseTokenProvider; diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 8b7878d4d3..1a2f3d773a 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -6,6 +6,8 @@ const models = require('../../models'); const signinEmail = require('./emails/signin'); const signupEmail = require('./emails/signup'); const subscribeEmail = require('./emails/subscribe'); +const SingleUseTokenProvider = require('./SingleUseTokenProvider'); +const MAGIC_LINK_TOKEN_VALIDITY = 4 * 60 * 60 * 1000; const ghostMailer = new mail.GhostMailer(); @@ -17,7 +19,7 @@ function createApiInstance(config) { auth: { getSigninURL: config.getSigninURL.bind(config), allowSelfSignup: config.getAllowSelfSignup(), - secret: config.getAuthSecret() + tokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY) }, mail: { transporter: { diff --git a/package.json b/package.json index 929637b881..4c72a2ab00 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@tryghost/kg-markdown-html-renderer": "2.0.2", "@tryghost/kg-mobiledoc-html-renderer": "3.0.1", "@tryghost/magic-link": "0.6.0", - "@tryghost/members-api": "0.28.2", + "@tryghost/members-api": "0.30.0", "@tryghost/members-csv": "0.3.0", "@tryghost/members-ssr": "0.8.5", "@tryghost/mw-session-from-token": "0.1.7", diff --git a/yarn.lock b/yarn.lock index 18e3d2bc08..f87d2ab775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -481,7 +481,7 @@ dependencies: "@tryghost/kg-clean-basic-html" "^1.0.6" -"@tryghost/magic-link@0.6.0": +"@tryghost/magic-link@0.6.0", "@tryghost/magic-link@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.6.0.tgz#a656e30385b60f44e5678819d6184c65f0757794" integrity sha512-KeE1dpCCAhZy34pjXirn1w1Oq4+dNsX9XqyZMvdruiYEB+88WkL/x//aNGCo84RoerTr1W8TGQF/1lh4ulrsZg== @@ -491,22 +491,12 @@ jsonwebtoken "^8.5.1" lodash "^4.17.15" -"@tryghost/magic-link@^0.4.13": - version "0.4.13" - resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.4.13.tgz#266940cec6f0fe837071621b43dfaa08f552d23c" - integrity sha512-m5rz9rDEeRwrDePRiw/5t3D/yfDkqtvhjWNRUqL0NXhvD/m8f8soP1m1fY46JVaUd35xGDa4BQRnFhXerNId3w== +"@tryghost/members-api@0.30.0": + version "0.30.0" + resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.30.0.tgz#459a82369786110e80e9d0287e1a1acf80edc688" + integrity sha512-meu0ipF0AGpY8f80yRbllW1XSpjQWeFwTR0Coq0Mpzsw9u6Ryodppq2uUSxFCUKyb6BtJLW6q+pJAvgB+eV2Gg== dependencies: - bluebird "^3.5.5" - ghost-ignition "4.2.2" - jsonwebtoken "^8.5.1" - lodash "^4.17.15" - -"@tryghost/members-api@0.28.2": - version "0.28.2" - resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.28.2.tgz#9ed4464754f9b8e70d1d4e62e8e50ca139346b6d" - integrity sha512-LgfhjLpSUusyjrDIDreXEH8PqdIsQ97q77opl3MRITMhTUS9qz0Q6GfCpuZfq0nVQ1qGx79pPC4K3CQqNRK/4w== - dependencies: - "@tryghost/magic-link" "^0.4.13" + "@tryghost/magic-link" "^0.6.0" bluebird "^3.5.4" body-parser "^1.19.0" cookies "^0.8.0"